feat(import): generic transactional profile-import service + CLI
Core performs a GAME-AGNOSTIC transactional profile import; all FIFA17 semantics (manifest parse, wire ids, resourceId/nextItemId, squad extension v1, CardDefinitionId/OwnedItemId choice) stay in openfut-import-fifa17. Core sees only opaque ids and opaque extension bytes. services::import::apply_profile_import(pool, card_db, ProfileImportRequest): - ONE SQLite transaction installs profile + club + all owned cards + canonical squad + one opaque game extension; commits together or not at all. - Definition preflight (pre-tx): every owned card_id MUST resolve in loaded production content, so ownership never points at absent content. - Squad all-or-nothing (pre-tx): every active-squad OwnedItemId MUST be in the imported ownership set. - OwnedItemId uniqueness + generic extension bounds enforced pre-tx. - Core computes the canonical squad fingerprint itself (never adapter-supplied) and persists the extension atomically, exactly as the live squad-write path. Rerun identity via profiles.import_fingerprint (migration 0018, nullable): - identical source_fingerprint on an already-imported game -> idempotent no-op; - differing token -> fail (needs explicit update mode); - pre-existing non-imported profile -> never clobbered. So a crash after identity-seeding re-runs cleanly with no cleanup/reminting. CLI: 'openfut-core import <request.json>' loads production content packs, parses a generic request, applies. squad_fingerprint made pub(crate) for reuse. 8 import-service tests (happy path, idempotent rerun, fingerprint mismatch, missing-definition no-write, squad-not-owned, dup OwnedItemId, non-imported clobber guard, empty-owned). clippy -D warnings clean; full suite 159 green.
This commit is contained in:
+26
-1
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use openfut_core::{config, db, seed};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
@@ -30,6 +30,31 @@ async fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Opt-in generic import subcommand: `openfut-core import <request.json>`.
|
||||
// Reads a GAME-AGNOSTIC ProfileImportRequest (the importer adapter translates
|
||||
// FIFA17 source data into it), loads production content packs, runs preflight,
|
||||
// and applies one all-or-nothing transaction. FIFA17 semantics live entirely
|
||||
// in the adapter; Core only sees opaque ids + opaque extension bytes.
|
||||
if std::env::args().nth(1).as_deref() == Some("import") {
|
||||
let path = std::env::args()
|
||||
.nth(2)
|
||||
.context("usage: openfut-core import <request.json>")?;
|
||||
let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?;
|
||||
db::run_migrations(&pool).await?;
|
||||
let mut card_db = openfut_core::services::card_db::CardDb::load(&cfg.data_dir)?;
|
||||
for pack in &cfg.content_packs {
|
||||
card_db.load_pack(pack)?;
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read import request {path}"))?;
|
||||
let req: openfut_core::services::import::ProfileImportRequest =
|
||||
serde_json::from_str(&raw).context("parse import request JSON")?;
|
||||
let outcome =
|
||||
openfut_core::services::import::apply_profile_import(&pool, &card_db, &req).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&outcome)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("OpenFUT Core starting on {}", cfg.listen_addr);
|
||||
|
||||
let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?;
|
||||
|
||||
Reference in New Issue
Block a user