145 lines
3.7 KiB
Dart
145 lines
3.7 KiB
Dart
/// 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<String, dynamic> 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<String, dynamic> json, String key) {
|
|
final raw = value(json, key);
|
|
if (raw == null) {
|
|
throw FormatException('Missing required property "$key".');
|
|
}
|
|
return raw.toString();
|
|
}
|
|
|
|
static String? optionalString(Map<String, dynamic> json, String key) {
|
|
final raw = value(json, key);
|
|
return raw?.toString();
|
|
}
|
|
|
|
static DateTime requireDateTime(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<T> list<T>(
|
|
Map<String, dynamic> json,
|
|
String key,
|
|
T Function(Map<String, dynamic> json) fromJson,
|
|
) {
|
|
final raw = value(json, key);
|
|
if (raw is! List) {
|
|
return <T>[];
|
|
}
|
|
return raw
|
|
.whereType<Map>()
|
|
.map((entry) => fromJson(Map<String, dynamic>.from(entry)))
|
|
.toList(growable: false);
|
|
}
|
|
|
|
/// Resolves a .Net enum that can arrive either as its name or as its index.
|
|
static T enumValue<T>(
|
|
Map<String, dynamic> json,
|
|
String key,
|
|
List<T> 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]'), '');
|
|
}
|
|
}
|