uto_manager/lib/infrastructure/codecs/unit_tree_codec.dart

196 lines
6.7 KiB
Dart

import 'dart:convert';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
import 'package:uto_manager/domain/entities/echelon.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
/// Codec for serializing and deserializing the full unit tree (units +
/// position slots + personnel) to/from a structured JSON representation.
///
/// The JSON schema is:
/// ```json
/// {
/// "version": 1,
/// "exportedAt": "<ISO-8601>",
/// "units": [ { ...UnitNode fields } ],
/// "slots": [ { ...PositionSlot fields } ],
/// "personnel": [ { ...PersonnelRecord fields } ]
/// }
/// ```
class UnitTreeCodec {
const UnitTreeCodec();
static const int _schemaVersion = 1;
// ---------------------------------------------------------------------------
// Encode
// ---------------------------------------------------------------------------
/// Encodes [units], [slots], and [personnel] into a JSON string.
String encode({
required List<UnitNode> units,
required List<PositionSlot> slots,
required List<PersonnelRecord> personnel,
}) {
final map = encodeToMap(units: units, slots: slots, personnel: personnel);
return const JsonEncoder.withIndent(' ').convert(map);
}
/// Encodes [units], [slots], and [personnel] into a [Map].
Map<String, dynamic> encodeToMap({
required List<UnitNode> units,
required List<PositionSlot> slots,
required List<PersonnelRecord> personnel,
}) {
return {
'version': _schemaVersion,
'exportedAt': DateTime.now().toUtc().toIso8601String(),
'units': units.map(_encodeUnit).toList(),
'slots': slots.map(_encodeSlot).toList(),
'personnel': personnel.map(_encodePersonnel).toList(),
};
}
// ---------------------------------------------------------------------------
// Decode
// ---------------------------------------------------------------------------
/// Decodes a JSON string into a [UnitTreeSnapshot].
///
/// Throws [FormatException] if the JSON is malformed or the schema version
/// is unsupported.
UnitTreeSnapshot decode(String json) {
final dynamic raw = jsonDecode(json);
if (raw is! Map<String, dynamic>) {
throw const FormatException('Root JSON element must be an object.');
}
return decodeFromMap(raw);
}
/// Decodes a [Map] into a [UnitTreeSnapshot].
UnitTreeSnapshot decodeFromMap(Map<String, dynamic> map) {
final version = map['version'] as int?;
if (version == null || version != _schemaVersion) {
throw FormatException(
'Unsupported schema version: $version. Expected $_schemaVersion.',
);
}
final unitsList = (map['units'] as List<dynamic>? ?? [])
.cast<Map<String, dynamic>>()
.map(_decodeUnit)
.toList();
final slotsList = (map['slots'] as List<dynamic>? ?? [])
.cast<Map<String, dynamic>>()
.map(_decodeSlot)
.toList();
final personnelList = (map['personnel'] as List<dynamic>? ?? [])
.cast<Map<String, dynamic>>()
.map(_decodePersonnel)
.toList();
return UnitTreeSnapshot(
units: unitsList,
slots: slotsList,
personnel: personnelList,
);
}
// ---------------------------------------------------------------------------
// Unit encoding/decoding
// ---------------------------------------------------------------------------
Map<String, dynamic> _encodeUnit(UnitNode unit) => {
'unitId': unit.unitId,
'echelon': unit.echelon.name,
'branch': unit.branch.name,
'parentUnitId': unit.parentUnitId,
'name': unit.name,
'isKeyLeader': unit.isKeyLeader,
};
UnitNode _decodeUnit(Map<String, dynamic> map) {
return UnitNode(
unitId: map['unitId'] as String,
echelon: Echelon.values.firstWhere((e) => e.name == map['echelon']),
branch: DoctrinalBranch.values.firstWhere((b) => b.name == map['branch']),
parentUnitId: map['parentUnitId'] as String?,
name: map['name'] as String,
isKeyLeader: map['isKeyLeader'] as bool? ?? false,
);
}
// ---------------------------------------------------------------------------
// Slot encoding/decoding
// ---------------------------------------------------------------------------
Map<String, dynamic> _encodeSlot(PositionSlot slot) => {
'slotId': slot.slotId,
'unitId': slot.unitId,
'positionTitle': slot.positionTitle,
'dutyMos': slot.dutyMos.code,
'authorizedRank': slot.authorizedRank.name,
'isKeyLeader': slot.isKeyLeader,
'assignedPersonnelId': slot.assignedPersonnelId,
};
PositionSlot _decodeSlot(Map<String, dynamic> map) {
return PositionSlot(
slotId: map['slotId'] as String,
unitId: map['unitId'] as String,
positionTitle: map['positionTitle'] as String,
dutyMos: DutyMos.unvalidated(map['dutyMos'] as String),
authorizedRank: AuthorizedRank.values.firstWhere((r) => r.name == map['authorizedRank']),
isKeyLeader: map['isKeyLeader'] as bool? ?? false,
assignedPersonnelId: map['assignedPersonnelId'] as String?,
);
}
// ---------------------------------------------------------------------------
// Personnel encoding/decoding
// ---------------------------------------------------------------------------
Map<String, dynamic> _encodePersonnel(PersonnelRecord record) => {
'soldierId': record.soldierId,
'name': record.name,
'primaryMos': record.primaryMos.code,
'payGrade': record.payGrade.name,
'qualifications': record.qualifications,
'readinessState': record.readinessState.name,
'assignedSlotId': record.assignedSlotId,
};
PersonnelRecord _decodePersonnel(Map<String, dynamic> map) {
return PersonnelRecord(
soldierId: map['soldierId'] as String,
name: map['name'] as String,
primaryMos: DutyMos.unvalidated(map['primaryMos'] as String),
payGrade: AuthorizedRank.values.firstWhere((r) => r.name == map['payGrade']),
qualifications: (map['qualifications'] as List<dynamic>? ?? []).cast<String>(),
readinessState: ReadinessState.values.firstWhere(
(s) => s.name == map['readinessState'],
),
assignedSlotId: map['assignedSlotId'] as String?,
);
}
}
/// A decoded snapshot of the full unit tree.
class UnitTreeSnapshot {
const UnitTreeSnapshot({
required this.units,
required this.slots,
required this.personnel,
});
final List<UnitNode> units;
final List<PositionSlot> slots;
final List<PersonnelRecord> personnel;
}