mod types;
#[cfg(feature = "import")]
mod adapter;
#[cfg(feature = "import")]
mod decode;
#[cfg(feature = "import")]
mod gw;
#[cfg(feature = "import")]
mod gw_headerless;
#[cfg(feature = "import")]
mod listforge;
#[cfg(feature = "import")]
mod newrecruit_json;
#[cfg(feature = "import")]
mod newrecruit_simple;
#[cfg(feature = "import")]
mod newrecruit_text;
#[cfg(feature = "import")]
mod newrecruit_wtc;
#[cfg(feature = "import")]
mod resolve;
#[cfg(feature = "import")]
mod rosterizer;
pub use types::{
BattleSize, Candidate, Diagnostics, GameVersionRef, ParsedRoster, ParsedUnit, ParsedWargear,
ResolvedRef, Roster, RosterFormat, RosterLeaderAttachment, RosterPoints, RosterSource,
RosterUnit, RosterWargear, Warning, WarningCode,
};
#[cfg(feature = "import")]
pub use adapter::{format_id, select_adapter, FormatAdapter, ParseError};
#[cfg(feature = "import")]
pub use decode::{decode_listforge, DecodeError};
#[cfg(feature = "import")]
pub use gw::GwAdapter;
#[cfg(feature = "import")]
pub use gw_headerless::GwHeaderlessAdapter;
#[cfg(feature = "import")]
pub use listforge::ListForgeAdapter;
#[cfg(feature = "import")]
pub use newrecruit_json::NewRecruitJsonAdapter;
#[cfg(feature = "import")]
pub use newrecruit_simple::NewRecruitSimpleAdapter;
#[cfg(feature = "import")]
pub use newrecruit_wtc::{NewRecruitWtcCompactAdapter, NewRecruitWtcFullAdapter};
#[cfg(feature = "import")]
pub use resolve::resolve;
#[cfg(feature = "import")]
pub use rosterizer::RosterizerAdapter;
#[cfg(feature = "import")]
use crate::data::Dataset;
#[cfg(feature = "import")]
#[derive(Debug)]
pub enum ImportError {
Decode(DecodeError),
Parse(ParseError),
}
#[cfg(feature = "import")]
impl std::fmt::Display for ImportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImportError::Decode(e) => write!(f, "{e}"),
ImportError::Parse(e) => write!(f, "{e}"),
}
}
}
#[cfg(feature = "import")]
impl std::error::Error for ImportError {}
#[cfg(feature = "import")]
impl From<DecodeError> for ImportError {
fn from(e: DecodeError) -> Self {
ImportError::Decode(e)
}
}
#[cfg(feature = "import")]
impl From<ParseError> for ImportError {
fn from(e: ParseError) -> Self {
ImportError::Parse(e)
}
}
#[cfg(feature = "import")]
fn adapters() -> Vec<Box<dyn FormatAdapter>> {
vec![
Box::new(RosterizerAdapter),
Box::new(NewRecruitJsonAdapter),
Box::new(GwAdapter),
Box::new(NewRecruitWtcFullAdapter),
Box::new(NewRecruitWtcCompactAdapter),
Box::new(NewRecruitSimpleAdapter),
Box::new(GwHeaderlessAdapter),
Box::new(ListForgeAdapter),
]
}
#[cfg(feature = "import")]
pub fn import_listforge(input: &str, ds: &Dataset) -> Result<Roster, ImportError> {
let decoded = decode_listforge(input)?;
import_roster(&decoded, ds)
}
#[cfg(feature = "import")]
pub fn import_roster(decoded: &serde_json::Value, ds: &Dataset) -> Result<Roster, ImportError> {
let registry = adapters();
let adapter = select_adapter(decoded, ®istry)?;
let parsed = adapter.parse(decoded)?;
Ok(resolve(&parsed, ds, adapter.format()))
}
#[cfg(feature = "import")]
pub fn import_roster_text(input: &str, ds: &Dataset) -> Result<Roster, ImportError> {
let wrapped = serde_json::Value::String(input.to_string());
import_roster(&wrapped, ds)
}
#[cfg(feature = "import")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportFailureReason {
EmptyInput,
DecodeFailed,
NoAdapterMatched,
ParseFailed,
}
#[cfg(feature = "import")]
#[derive(Debug, Clone)]
pub struct AdapterTrial {
pub id: RosterFormat,
pub matched: bool,
pub reason: Option<String>,
}
#[cfg(feature = "import")]
#[derive(Debug)]
pub enum ImportResult {
Ok {
roster: Roster,
format: RosterFormat,
},
Err {
reason: ImportFailureReason,
message: String,
trials: Vec<AdapterTrial>,
},
}
#[cfg(feature = "import")]
fn looks_like_listforge_encoded(input: &str) -> bool {
if input.contains("/listforge/") {
return true;
}
let lower = input
.get(..8)
.map(str::to_ascii_lowercase)
.unwrap_or_default();
if lower.starts_with("http://") || lower.starts_with("https://") {
return true;
}
input.starts_with("H4sIA")
}
#[cfg(feature = "import")]
pub fn try_import_roster(input: &str, ds: &Dataset) -> ImportResult {
let trimmed = input.trim();
if trimmed.is_empty() {
return ImportResult::Err {
reason: ImportFailureReason::EmptyInput,
message: "input is empty".to_string(),
trials: Vec::new(),
};
}
let decoded: serde_json::Value = if looks_like_listforge_encoded(trimmed) {
match decode_listforge(trimmed) {
Ok(v) => v,
Err(e) => {
let message = e.to_string();
return ImportResult::Err {
reason: ImportFailureReason::DecodeFailed,
message: format!("failed to decode ListForge payload: {message}"),
trials: vec![AdapterTrial {
id: RosterFormat::Listforge,
matched: false,
reason: Some(message),
}],
};
}
}
} else if trimmed.starts_with('{') || trimmed.starts_with('[') {
match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(e) => {
return ImportResult::Err {
reason: ImportFailureReason::DecodeFailed,
message: format!("input looks like JSON but failed to parse: {e}"),
trials: Vec::new(),
};
}
}
} else {
serde_json::Value::String(input.to_string())
};
let registry = adapters();
let mut trials: Vec<AdapterTrial> = Vec::new();
for adapter in registry.iter() {
if !adapter.detect(&decoded) {
trials.push(AdapterTrial {
id: adapter.format(),
matched: false,
reason: None,
});
continue;
}
match adapter.parse(&decoded) {
Ok(parsed) => {
let roster = resolve(&parsed, ds, adapter.format());
return ImportResult::Ok {
roster,
format: adapter.format(),
};
}
Err(e) => {
let message = e.to_string();
let id = adapter.format();
trials.push(AdapterTrial {
id,
matched: true,
reason: Some(message.clone()),
});
return ImportResult::Err {
reason: ImportFailureReason::ParseFailed,
message: format!("{}: {message}", crate::import::format_id(id)),
trials,
};
}
}
}
let count = trials.len();
ImportResult::Err {
reason: ImportFailureReason::NoAdapterMatched,
message: format!("tried {count} formats, none recognised the input"),
trials,
}
}