From f2dcba0fe272791a3fb51b91be97611511f42f99 Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Tue, 9 Jun 2026 11:09:51 +0200 Subject: [PATCH] feat(data): add PokemonDto with single-source JSON parsing + tests Co-Authored-By: Claude Opus 4.8 --- lib/data/dto/pokemon_dto.dart | 93 +++++++++++++++++++++++++++++++++ test/data/pokemon_dto_test.dart | 83 +++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 lib/data/dto/pokemon_dto.dart create mode 100644 test/data/pokemon_dto_test.dart diff --git a/lib/data/dto/pokemon_dto.dart b/lib/data/dto/pokemon_dto.dart new file mode 100644 index 0000000..8229d80 --- /dev/null +++ b/lib/data/dto/pokemon_dto.dart @@ -0,0 +1,93 @@ +import '../../domain/entities/pokemon.dart'; + +/// Conversion JSON <-> entité Pokemon. Unique endroit de parsing. +class PokemonDto { + PokemonDto._(); + + /// Mappe un nom de type français (API Tyradex) vers l'enum. + static PokemonType frenchTypeToEnum(String frenchType) { + const map = { + 'Normal': PokemonType.normal, + 'Combat': PokemonType.fighting, + 'Vol': PokemonType.flying, + 'Poison': PokemonType.poison, + 'Sol': PokemonType.ground, + 'Roche': PokemonType.rock, + 'Insecte': PokemonType.bug, + 'Spectre': PokemonType.ghost, + 'Acier': PokemonType.steel, + 'Feu': PokemonType.fire, + 'Eau': PokemonType.water, + 'Plante': PokemonType.grass, + 'Électrik': PokemonType.electric, + 'Psy': PokemonType.psychic, + 'Glace': PokemonType.ice, + 'Dragon': PokemonType.dragon, + 'Ténèbres': PokemonType.dark, + 'Fée': PokemonType.fairy, + }; + return map[frenchType] ?? PokemonType.unknown; + } + + /// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste). + /// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`. + static Pokemon fromTyradexJson(Map json, {int? fallbackId}) { + final id = (json['pokedex_id'] as int?) ?? fallbackId!; + final nameMap = json['name'] as Map?; + final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown'; + + final List types = json['types'] ?? []; + final type1 = + types.isNotEmpty ? frenchTypeToEnum(types[0]['name']) : PokemonType.unknown; + final type2 = types.length > 1 ? frenchTypeToEnum(types[1]['name']) : null; + + final Map? stats = json['stats']; + return Pokemon( + name: name, + id: id, + type1: type1, + type2: type2, + hp: stats?['hp'] ?? 0, + atk: stats?['atk'] ?? 0, + def: stats?['def'] ?? 0, + spd: stats?['vit'] ?? 0, // 'vit' = vitesse chez Tyradex + description: json['category'], + ); + } + + /// Sérialise pour SQLite. + static Map toDb(Pokemon p) { + return { + 'name': p.name, + 'id': p.id, + 'type1': p.type1.name, + 'type2': p.type2?.name, + 'hp': p.hp, + 'atk': p.atk, + 'def': p.def, + 'spd': p.spd, + 'description': p.description, + 'isCaught': p.isCaught ? 1 : 0, + 'isSeen': p.isSeen ? 1 : 0, + }; + } + + /// Reconstruit depuis une ligne SQLite. + static Pokemon fromDb(Map row) { + return Pokemon( + name: row['name'], + id: row['id'], + type1: PokemonType.values.firstWhere((e) => e.name == row['type1']), + type2: row['type2'] != null + ? PokemonType.values.firstWhere((e) => e.name == row['type2']) + : null, + hp: row['hp'] ?? 0, + atk: row['atk'] ?? 0, + def: row['def'] ?? 0, + spd: row['spd'] ?? 0, + description: row['description'], + isCaught: row['isCaught'] == 1 || row['isCaught'] == true, + isSeen: row['isSeen'] == 1 || row['isSeen'] == true, + ); + } +} diff --git a/test/data/pokemon_dto_test.dart b/test/data/pokemon_dto_test.dart new file mode 100644 index 0000000..e80d7f8 --- /dev/null +++ b/test/data/pokemon_dto_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pokeguess/domain/entities/pokemon.dart'; +import 'package:pokeguess/data/dto/pokemon_dto.dart'; + +void main() { + group('PokemonDto.fromTyradexJson', () { + test('parse un Pokémon complet avec deux types', () { + final json = { + 'pokedex_id': 6, + 'name': {'fr': 'Dracaufeu', 'en': 'Charizard'}, + 'types': [ + {'name': 'Feu'}, + {'name': 'Vol'}, + ], + 'stats': {'hp': 78, 'atk': 84, 'def': 78, 'vit': 100}, + 'category': 'Pokémon Flamme', + }; + final p = PokemonDto.fromTyradexJson(json); + expect(p.id, 6); + expect(p.name, 'Dracaufeu'); + expect(p.type1, PokemonType.fire); + expect(p.type2, PokemonType.flying); + expect(p.hp, 78); + expect(p.spd, 100); + expect(p.description, 'Pokémon Flamme'); + }); + + test('utilise fallbackId quand pokedex_id absent', () { + final json = { + 'name': {'fr': 'Bulbizarre'}, + 'types': [{'name': 'Plante'}], + 'stats': {'hp': 45, 'atk': 49, 'def': 49, 'vit': 45}, + 'category': 'Pokémon Graine', + }; + final p = PokemonDto.fromTyradexJson(json, fallbackId: 1); + expect(p.id, 1); + expect(p.type2, isNull); + expect(p.type1, PokemonType.grass); + }); + + test('type inconnu mappé sur PokemonType.unknown', () { + final json = { + 'pokedex_id': 999, + 'name': {'fr': 'Test'}, + 'types': [{'name': 'TypeInexistant'}], + 'stats': {'hp': 1, 'atk': 1, 'def': 1, 'vit': 1}, + }; + final p = PokemonDto.fromTyradexJson(json); + expect(p.type1, PokemonType.unknown); + expect(p.description, isNull); + }); + }); + + group('PokemonDto round-trip DB', () { + test('toDb puis fromDb reconstruit le Pokémon', () { + const original = Pokemon( + name: 'pikachu', + id: 25, + type1: PokemonType.electric, + type2: null, + hp: 35, + atk: 55, + def: 40, + spd: 90, + description: 'Souris', + isCaught: true, + isSeen: true, + ); + final row = PokemonDto.toDb(original); + expect(row['type1'], 'electric'); + expect(row['type2'], isNull); + expect(row['isCaught'], 1); + + final restored = PokemonDto.fromDb(row); + expect(restored.name, 'pikachu'); + expect(restored.id, 25); + expect(restored.type1, PokemonType.electric); + expect(restored.type2, isNull); + expect(restored.isCaught, true); + expect(restored.isSeen, true); + }); + }); +}