use crate::events::{DraftCompleteEvent, EventMetadata, GameEvent};
use crate::log::entry::LogEntry;
use crate::parsers::api_common;
const COMPLETE_DRAFT_MARKER: &str = "DraftCompleteDraft";
pub fn try_parse(
entry: &LogEntry,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
) -> Option<GameEvent> {
let body = &entry.body;
if !body.contains(COMPLETE_DRAFT_MARKER) {
return None;
}
let parsed = api_common::parse_json_from_body(body, "DraftCompleteDraft")?;
let draft_id_from_body = extract_draft_id_from_body(body);
let payload = build_payload(&parsed, draft_id_from_body.as_deref());
let metadata = EventMetadata::new(timestamp, body.as_bytes().to_vec());
Some(GameEvent::DraftComplete(DraftCompleteEvent::new(
metadata, payload,
)))
}
fn build_payload(
parsed: &serde_json::Value,
draft_id_from_body: Option<&str>,
) -> serde_json::Value {
let draft_id = parsed
.get("DraftId")
.or_else(|| parsed.get("draftId"))
.or_else(|| parsed.get("id"))
.and_then(serde_json::Value::as_str)
.or(draft_id_from_body)
.unwrap_or("");
let event_name = api_common::extract_event_name(parsed);
serde_json::json!({
"type": "draft_complete",
"draft_id": draft_id,
"event_name": event_name,
"raw_complete_draft": parsed.clone(),
})
}
fn extract_draft_id_from_body(body: &str) -> Option<String> {
let marker = "DraftCompleteDraft(";
let start = body.find(marker)? + marker.len();
let remaining = body.get(start..)?;
let end = remaining.find(')')?;
Some(remaining.get(..end)?.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::PerformanceClass;
use crate::parsers::test_helpers::{
draft_complete_payload, test_timestamp, unity_entry, EntryHeader,
};
mod request_format {
use super::*;
#[test]
fn test_try_parse_request_basic() {
let body = r#"[UnityCrossThreadLogger]==> DraftCompleteDraft {"id":"abc-123-def","request":"{\"EventName\":\"PremierDraft_MKM_20260201\",\"IsBotDraft\":false}"}"#;
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "abc-123-def");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_try_parse_request_traditional_draft() {
let body = r#"[UnityCrossThreadLogger]==> DraftCompleteDraft {"id":"trad-456","request":"{\"EventName\":\"TradDraft_DSK_20260115\",\"IsBotDraft\":false}"}"#;
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "trad-456");
assert_eq!(payload["event_name"], "TradDraft_DSK_20260115");
}
#[test]
fn test_try_parse_request_empty_request_string() {
let body = r#"[UnityCrossThreadLogger]==> DraftCompleteDraft {"id":"empty-req","request":"{}"}"#;
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "empty-req");
assert_eq!(payload["event_name"], "");
}
}
mod response_format {
use super::*;
#[test]
fn test_try_parse_response_basic() {
let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
<== DraftCompleteDraft(abc-123-def)\n\
{\"CourseId\":\"course-456\",\
\"InternalEventName\":\"PremierDraft_MKM_20260201\",\
\"CurrentModule\":\"DeckSelect\",\
\"CardPool\":[98535,98381]}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "abc-123-def");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_try_parse_response_preserves_card_pool() {
let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
<== DraftCompleteDraft(pool-test)\n\
{\"InternalEventName\":\"PremierDraft_ECL_20260120\",\
\"CardPool\":[98535,98381,98366]}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
let raw = &payload["raw_complete_draft"];
assert_eq!(raw["CardPool"][0], 98535);
assert_eq!(raw["CardPool"][2], 98366);
}
#[test]
fn test_try_parse_response_event_name_fallback() {
let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
<== DraftCompleteDraft(fallback-test)\n\
{\"EventName\":\"QuickDraft_MKM_20260201\"}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["event_name"], "QuickDraft_MKM_20260201");
}
}
mod flat_format {
use super::*;
#[test]
fn test_try_parse_flat_basic() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"abc-123-def\",\n\
\"EventName\": \"PremierDraft_MKM_20260201\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "abc-123-def");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_try_parse_flat_traditional() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"trad-456\",\n\
\"EventName\": \"TradDraft_DSK_20260115\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "trad-456");
assert_eq!(payload["event_name"], "TradDraft_DSK_20260115");
}
#[test]
fn test_try_parse_flat_quick_draft() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"quick-789\",\n\
\"EventName\": \"QuickDraft_MKM_20260201\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "quick-789");
assert_eq!(payload["event_name"], "QuickDraft_MKM_20260201");
}
#[test]
fn test_try_parse_flat_lowercase_draft_id() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"draftId\": \"lowercase-123\",\n\
\"EventName\": \"PremierDraft_MKM_20260201\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "lowercase-123");
}
#[test]
fn test_try_parse_flat_internal_event_name() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"intern-456\",\n\
\"InternalEventName\": \"PremierDraft_MKM_20260201\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
}
mod missing_fields {
use super::*;
#[test]
fn test_try_parse_missing_draft_id() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"EventName\": \"PremierDraft_MKM_20260201\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_try_parse_missing_event_name() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"no-event-name\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "no-event-name");
assert_eq!(payload["event_name"], "");
}
#[test]
fn test_try_parse_minimal_payload() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["draft_id"], "");
assert_eq!(payload["event_name"], "");
}
}
mod metadata {
use super::*;
#[test]
fn test_try_parse_preserves_raw_bytes() {
let body = r#"[UnityCrossThreadLogger]==> DraftCompleteDraft {"id":"raw-test","request":"{\"EventName\":\"PremierDraft_MKM_20260201\"}"}"#;
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.metadata().raw_bytes(), body.as_bytes());
}
#[test]
fn test_try_parse_stores_timestamp() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\"DraftId\": \"ts-test\"}";
let entry = unity_entry(body);
let ts = Some(test_timestamp());
let result = try_parse(&entry, ts);
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.metadata().timestamp(), ts);
}
#[test]
fn test_try_parse_preserves_raw_payload() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\n\
\"DraftId\": \"raw-payload\",\n\
\"ExtraField\": \"preserved\"\n\
}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["raw_complete_draft"]["ExtraField"], "preserved");
}
}
mod non_matching {
use super::*;
#[test]
fn test_try_parse_unrelated_entry_returns_none() {
let body = "[UnityCrossThreadLogger]greToClientEvent\n{\"data\": 1}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_empty_body_returns_none() {
let body = "[UnityCrossThreadLogger]";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_bot_draft_entry_returns_none() {
let body = "[UnityCrossThreadLogger]BotDraft_DraftPick\n\
{\"PickInfo\": {\"CardId\": 12345}}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_human_draft_entry_returns_none() {
let body = "[UnityCrossThreadLogger]Draft.Notify\n\
{\"SelfPack\": 0, \"SelfPick\": 0, \
\"PackCards\": \"12345\"}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_malformed_json_returns_none() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{broken json!!!}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_marker_only_no_json_returns_none() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_connection_manager_header_returns_none() {
let entry = LogEntry {
header: EntryHeader::ConnectionManager,
body: "[ConnectionManager]some connection message".to_owned(),
};
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_old_underscore_marker_returns_none() {
let body = "[UnityCrossThreadLogger]Draft_CompleteDraft\n\
{\"DraftId\": \"old-marker\", \
\"EventName\": \"PremierDraft_MKM_20260201\"}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
}
mod timestamp_in_header {
use super::*;
#[test]
fn test_try_parse_with_timestamp_prefix() {
let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM \
DraftCompleteDraft\n\
{\"DraftId\": \"ts-prefix\", \
\"EventName\": \"PremierDraft_MKM_20260201\"}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = draft_complete_payload(event);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "ts-prefix");
}
}
mod performance_class {
use super::*;
#[test]
fn test_draft_complete_event_is_durable_per_event() {
let body = "[UnityCrossThreadLogger]DraftCompleteDraft\n\
{\"DraftId\": \"perf-test\"}";
let entry = unity_entry(body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.performance_class(), PerformanceClass::DurablePerEvent);
}
}
mod helpers {
use super::*;
#[test]
fn test_build_payload_request_format() {
let parsed = serde_json::json!({
"id": "req-123",
"request": "{\"EventName\":\"PremierDraft_MKM_20260201\",\"IsBotDraft\":false}"
});
let payload = build_payload(&parsed, None);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "req-123");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_build_payload_response_format() {
let parsed = serde_json::json!({
"CourseId": "course-456",
"InternalEventName": "PremierDraft_MKM_20260201",
"CurrentModule": "DeckSelect"
});
let payload = build_payload(&parsed, Some("resp-789"));
assert_eq!(payload["draft_id"], "resp-789");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_build_payload_flat_format() {
let parsed = serde_json::json!({
"DraftId": "test-id",
"EventName": "PremierDraft_MKM_20260201"
});
let payload = build_payload(&parsed, None);
assert_eq!(payload["type"], "draft_complete");
assert_eq!(payload["draft_id"], "test-id");
assert_eq!(payload["event_name"], "PremierDraft_MKM_20260201");
}
#[test]
fn test_build_payload_empty() {
let parsed = serde_json::json!({});
let payload = build_payload(&parsed, None);
assert_eq!(payload["draft_id"], "");
assert_eq!(payload["event_name"], "");
}
#[test]
fn test_extract_draft_id_from_body_response() {
let body = "<== DraftCompleteDraft(abc-123-def)\n{\"some\":\"json\"}";
assert_eq!(
extract_draft_id_from_body(body),
Some("abc-123-def".to_owned()),
);
}
#[test]
fn test_extract_draft_id_from_body_request_returns_none() {
let body = r#"==> DraftCompleteDraft {"id":"abc-123-def"}"#;
assert!(extract_draft_id_from_body(body).is_none());
}
#[test]
fn test_extract_draft_id_from_body_no_marker() {
assert!(extract_draft_id_from_body("no marker here").is_none());
}
}
}