import 'runtime_command.dart'; import 'runtime_node.dart'; import '../protocol/runtime_protocol.dart'; class NodeUpdate { const NodeUpdate({required this.id, required this.props}); final String id; final Map props; static NodeUpdate fromMap(Map map) { RuntimeProtocolSchema.ensureKnownKeys( map, allowed: RuntimeProtocolSchema.nodeUpdateFields, context: 'NodeUpdate', ); final id = map[RuntimeProtocolField.id]; if (id is! String || id.isEmpty) { throw const FormatException('NodeUpdate.id must be a string'); } final props = map[RuntimeProtocolField.props]; if (props is! Map) { throw const FormatException('NodeUpdate.props must be a map'); } final typedProps = Map.from(props); RuntimeProtocolSchema.ensureKnownKeys( typedProps, allowed: RuntimeProtocolSchema.nodePropsFields, context: 'RuntimeNode.props', ); return NodeUpdate(id: id, props: typedProps); } } class NodeRemove { const NodeRemove({required this.id}); final String id; static NodeRemove fromValue(Object? value) { if (value is String && value.isNotEmpty) { return NodeRemove(id: value); } if (value is Map) { RuntimeProtocolSchema.ensureKnownKeys( value, allowed: RuntimeProtocolSchema.nodeRemoveFields, context: 'NodeRemove', ); final id = value[RuntimeProtocolField.id]; if (id is String && id.isNotEmpty) { return NodeRemove(id: id); } } throw const FormatException('NodeRemove must be an id string or {id}'); } } class NodeDiff { const NodeDiff({ this.creates = const [], this.updates = const [], this.removes = const [], }); final List creates; final List updates; final List removes; static NodeDiff empty = const NodeDiff(); static NodeDiff fromMap(Object? value) { if (value == null) { return NodeDiff.empty; } if (value is! Map) { throw const FormatException('NodeDiff must be a map'); } RuntimeProtocolSchema.ensureKnownKeys( value, allowed: RuntimeProtocolSchema.nodeDiffFields, context: 'NodeDiff', ); return NodeDiff( creates: _readList( value[RuntimeProtocolField.creates], (item) => RuntimeNode.fromMap(Map.from(item as Map)), ), updates: _readList( value[RuntimeProtocolField.updates], (item) => NodeUpdate.fromMap(Map.from(item as Map)), ), removes: _readList( value[RuntimeProtocolField.removes], NodeRemove.fromValue, ), ); } static List _readList(Object? value, T Function(Object? value) mapper) { if (value == null) { return const []; } if (value is List) { return value.map(mapper).toList(growable: false); } if (value is Map && value.isEmpty) { return const []; } if (value is Map && value.keys.every(_isPositiveIntegerKey)) { final entries = value.entries.toList() ..sort( (a, b) => int.parse( a.key.toString(), ).compareTo(int.parse(b.key.toString())), ); return entries .map((entry) => mapper(entry.value)) .toList(growable: false); } throw const FormatException('Diff field must be a list'); } static bool _isPositiveIntegerKey(Object? key) { final value = int.tryParse(key.toString()); return value != null && value > 0; } } class GameDiff { const GameDiff({ required this.render, required this.ui, required this.commands, }); final NodeDiff render; final NodeDiff ui; final List commands; static const empty = GameDiff( render: NodeDiff(), ui: NodeDiff(), commands: [], ); static GameDiff fromMap(Map map) { RuntimeProtocolSchema.ensureKnownKeys( map, allowed: RuntimeProtocolSchema.gameDiffFields, context: 'GameDiff', ); final commandsValue = map[RuntimeProtocolField.commands]; final commands = commandsValue == null ? const [] : NodeDiff._readList( commandsValue, (item) => RuntimeCommand.fromMap(Map.from(item as Map)), ); return GameDiff( render: NodeDiff.fromMap(map[RuntimeProtocolField.render]), ui: NodeDiff.fromMap(map[RuntimeProtocolField.ui]), commands: commands, ); } }