/// Small helper that makes reading .Net json payloads defensive. /// /// The backend is still in development, so property names might arrive in /// PascalCase or camelCase and single properties might be missing. These /// helpers keep the dto factories short and readable. class JsonReader { const JsonReader._(); /// Looks a key up, ignoring casing differences between .Net and Dart. static dynamic value(Map json, String key) { if (json.containsKey(key)) { return json[key]; } final lowerKey = key.toLowerCase(); for (final entry in json.entries) { if (entry.key.toLowerCase() == lowerKey) { return entry.value; } } return null; } static String requireString(Map json, String key) { final raw = value(json, key); if (raw == null) { throw FormatException('Missing required property "$key".'); } return raw.toString(); } static String? optionalString(Map json, String key) { final raw = value(json, key); return raw?.toString(); } static DateTime requireDateTime(Map json, String key) { final parsed = optionalDateTime(json, key); if (parsed == null) { throw FormatException('Missing or invalid date property "$key".'); } return parsed; } static DateTime? optionalDateTime(Map json, String key) { final raw = value(json, key); if (raw == null) { return null; } if (raw is DateTime) { return raw; } return DateTime.tryParse(raw.toString()); } static bool? optionalBool(Map json, String key) { final raw = value(json, key); if (raw == null) { return null; } if (raw is bool) { return raw; } final text = raw.toString().toLowerCase(); if (text == 'true' || text == '1') { return true; } if (text == 'false' || text == '0') { return false; } return null; } static int? optionalInt(Map json, String key) { final raw = value(json, key); if (raw == null) { return null; } if (raw is int) { return raw; } if (raw is num) { return raw.toInt(); } return int.tryParse(raw.toString()); } /// Maps a json array onto a list of models, tolerating a missing property. static List list( Map json, String key, T Function(Map json) fromJson, ) { final raw = value(json, key); if (raw is! List) { return []; } return raw .whereType() .map((entry) => fromJson(Map.from(entry))) .toList(growable: false); } /// Resolves a .Net enum that can arrive either as its name or as its index. static T enumValue( Map json, String key, List values, String Function(T value) nameOf, T fallback, ) { final raw = value(json, key); if (raw == null) { return fallback; } if (raw is num) { final index = raw.toInt(); if (index >= 0 && index < values.length) { return values[index]; } return fallback; } final text = raw.toString().trim().toLowerCase(); final normalized = _normalizeEnumName(text); for (final candidate in values) { if (_normalizeEnumName(nameOf(candidate).toLowerCase()) == normalized) { return candidate; } } final index = int.tryParse(text); if (index != null && index >= 0 && index < values.length) { return values[index]; } return fallback; } /// Removes separators so that `AC_API` also matches `acApi`. static String _normalizeEnumName(String value) { return value.replaceAll(RegExp(r'[^a-z0-9]'), ''); } }