//! `openfut-import-fifa17` — real FIFA 17 profile import. //! //! This phase implements the read-only analysis and `--emit-content` (public //! content pack + host catalog + private manifest). `--apply` is recognised but //! deliberately refuses (and exits nonzero) so no Core DB / identity-store //! writes happen before the apply phase lands. use anyhow::{bail, Context, Result}; use std::collections::BTreeSet; use std::path::Path; use std::process::ExitCode; use openfut_import_fifa17::apply::{apply, plan_apply, ApplyPaths}; use openfut_import_fifa17::{ analyze, emit_content, fingerprint, load_roster, model::Profile, Entities, }; fn main() -> ExitCode { match run() { Ok(code) => code, Err(e) => { eprintln!("import-fifa17: {e:#}"); ExitCode::FAILURE } } } fn print_help() { eprintln!( "openfut-import-fifa17 --profile [options]\n\ \n\ Read-only analysis of a real FIFA 17 Python profile for a faithful Core\n\ import, with optional public content emission. Exits nonzero on unsafe blockers.\n\ \n\ --profile (required) source fifa17_profile.json\n\ --roster roster.json for names (default fifa17-recon/data/roster.json)\n\ --tables tables dir (default fifa17-recon/data/tables)\n\ --defer-conflict explicitly approve deferring a reviewed resourceId conflict (repeatable)\n\ --emit-content write the production content pack + host catalog + private manifest\n\ --apply apply across the identity store + Core (requires --emit-content)\n\ --allow-deferred-players-for-staging import despite deferred players (STAGED only; never default)\n\ --core-bin openfut-core binary for the generic import transaction\n\ --core-db DATABASE_URL for the target Core DB\n\ --core-data DATA_DIR base catalog for Core (default data)\n\ --identity-store openfut-identity JSON store to seed\n" ); } fn run() -> Result { let mut profile_path: Option = None; let mut roster_path = "fifa17-recon/data/roster.json".to_string(); let mut tables_dir = "fifa17-recon/data/tables".to_string(); let mut emit_dir: Option = None; let mut approved: BTreeSet = BTreeSet::new(); let mut do_apply = false; let mut allow_staging = false; let mut core_bin: Option = None; let mut core_db: Option = None; let mut core_data = "data".to_string(); let mut identity_store: Option = None; let mut args = std::env::args().skip(1); while let Some(a) = args.next() { match a.as_str() { "--profile" => profile_path = Some(args.next().context("--profile needs a path")?), "--roster" => roster_path = args.next().context("--roster needs a path")?, "--tables" => tables_dir = args.next().context("--tables needs a dir")?, "--emit-content" => emit_dir = Some(args.next().context("--emit-content needs a dir")?), "--defer-conflict" => { let rid = args.next().context("--defer-conflict needs a resourceId")?; approved.insert( rid.parse::() .with_context(|| format!("--defer-conflict {rid} is not an integer"))?, ); } "--dry-run" => {} "--apply" => do_apply = true, "--allow-deferred-players-for-staging" => allow_staging = true, "--core-bin" => core_bin = Some(args.next().context("--core-bin needs a path")?), "--core-db" => core_db = Some(args.next().context("--core-db needs a DATABASE_URL")?), "--core-data" => core_data = args.next().context("--core-data needs a dir")?, "--identity-store" => { identity_store = Some(args.next().context("--identity-store needs a path")?) } "-h" | "--help" => { print_help(); return Ok(ExitCode::SUCCESS); } other => bail!("unknown argument: {other} (try --help)"), } } let profile_path = profile_path.context("--profile is required (try --help)")?; let raw = std::fs::read(&profile_path).with_context(|| format!("reading profile {profile_path}"))?; let profile = Profile::from_json_str(&String::from_utf8_lossy(&raw))?; let roster = load_roster(&roster_path)?; let entities = Entities::from_tables_dir(&tables_dir)?; let report = analyze(&profile, &roster, &entities, &approved); print!("{report}"); if let Some(dir) = &emit_dir { if report.has_blockers() { bail!("not emitting content: unsafe blockers present (resolve them first)"); } let fp = fingerprint(&raw); let sum = emit_content(&report, Path::new(dir), &fp)?; println!("\nEMITTED (fingerprint {fp})"); println!( " content pack : {} ({} definitions)", sum.content_pack.display(), sum.definitions ); println!( " host catalog : {} ({} entries)", sum.host_catalog.display(), sum.catalog_entries ); println!( " manifest : {} (supported_instances={} deferred_instances={})", sum.manifest.display(), sum.supported_instances, sum.deferred_instances ); println!( " reclassify : {} (run `openfut-core reclassify ` to correct \ content_kind on a club imported before the taxonomy existed)", sum.reclassify.display() ); println!( " non-player : {} definition(s), {} instance(s) (consumable/staff)", sum.non_player_definitions, sum.non_player_instances ); } if do_apply { let dir = emit_dir .as_deref() .context("--apply requires --emit-content (it reads the emitted content pack)")?; if report.has_blockers() { bail!("not applying: unsafe blockers present (resolve them first)"); } let raw_value: serde_json::Value = serde_json::from_slice(&raw).context("parse profile JSON for squad wire")?; let fp = fingerprint(&raw); let plan = plan_apply(&report, &raw_value, &fp)?; let base = Path::new(dir); let paths = ApplyPaths { core_bin: core_bin .context("--apply requires --core-bin ")? .into(), core_db_url: core_db.context("--apply requires --core-db ")?, content_pack: base.join("content/fifa17-production-cards.json"), data_dir: core_data, identity_store: identity_store .context("--apply requires --identity-store ")? .into(), request_out: base.join("manifest/fifa17-core-import-request.json"), completion_out: base.join("manifest/fifa17-import-completion.json"), }; let outcome = apply(&plan, &paths, allow_staging)?; println!( "\nAPPLIED (core_outcome={} staging={})", outcome.core_outcome, outcome.staging ); println!( " identity mappings installed: {} ; watermark: {}", outcome.mappings_installed, outcome.watermark ); println!( " supported instances: {} ; deferred instances: {}", outcome.supported_instances, outcome.deferred_instances ); println!(" request : {}", paths.request_out.display()); println!(" completion : {}", paths.completion_out.display()); } if report.has_blockers() { eprintln!("import-fifa17: FAILED — unsafe blockers present (see above)"); Ok(ExitCode::FAILURE) } else { Ok(ExitCode::SUCCESS) } }