MissionStep.fromJson constructor

MissionStep.fromJson(
  1. Map<String, dynamic> json
)

Implementation

factory MissionStep.fromJson(Map<String, dynamic> json) {
  final rawParams = json['params'];
  final params = rawParams is Map
      ? Map<String, dynamic>.from(rawParams)
      : const <String, dynamic>{};
  final rawBounds = json['bounds'];
  final rawTokens = json['target_tokens'];
  final rawAction = json['action'];
  if (rawAction is! String || rawAction.trim().isEmpty) {
    throw const FormatException('mission step has no string `action`');
  }
  // Every optional field is read TYPED: a wrong-typed value degrades to
  // "no constraint" instead of throwing, so one malformed field cannot drop
  // the whole frame (the backend would wait 25s for a terminal status).
  String? str(String key) {
    final v = json[key];
    return v is String ? v : null;
  }

  final rawNth = json['nth_match'];
  return MissionStep(
    action: rawAction,
    targetStableId: str('target_stable_id'),
    targetIdentifier: str('target_identifier'),
    targetText: str('target_text'),
    targetRole: str('target_role'),
    // The backend sends bounds as floats (`[216.0, 720.0, …]`); a lazy
    // `cast<int>()` throws on first read, failing every mission.
    bounds: rawBounds is List &&
            rawBounds.length == 4 &&
            rawBounds.every((e) => e is num && e.isFinite)
        ? rawBounds.map((e) => (e as num).round()).toList(growable: false)
        : null,
    kind: str('kind'),
    targetTokens: rawTokens is List
        ? rawTokens.map((t) => t.toString()).toList(growable: false)
        : null,
    insideStableId: str('inside_stable_id'),
    insideText: str('inside_text'),
    insideRole: str('inside_role'),
    forbidText: str('forbid_text'),
    nthMatch: rawNth is num && rawNth.isFinite && rawNth == rawNth.floor()
        ? rawNth.toInt()
        : null,
    params: params,
  );
}