use serde::de::DeserializeOwned;
use crate::reload::{FileKind, KINDS};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Refusal {
pub kind: &'static str,
pub file: String,
pub field: Option<String>,
pub reason: String,
}
impl Refusal {
pub fn sentence(&self) -> String {
match &self.field {
Some(field) => format!("{} ({field}): {}", self.file, self.reason),
None => format!("{}: {}", self.file, self.reason),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Check {
Valid,
Refused(Refusal),
Unchecked,
}
fn run<T: DeserializeOwned>(
text: &str,
parse: fn(&str) -> Result<T, String>,
) -> Result<(), (Option<String>, String)> {
match parse(text) {
Ok(_) => Ok(()),
Err(reason) => Err((field_of::<T>(text), reason)),
}
}
fn field_of<T: DeserializeOwned>(text: &str) -> Option<String> {
let mut de = ron::de::Deserializer::from_str(text).ok()?;
let err = serde_path_to_error::deserialize::<_, T>(&mut de).err()?;
let path = err.path().to_string().replace(".?", "");
let (named, key_in_path) = match err.inner() {
ron::Error::MissingStructField { field, .. } => (Some(field.to_string()), false),
ron::Error::NoSuchStructField { found, .. } => (Some(found.to_string()), true),
_ => (None, false),
};
match (path.as_str(), named) {
(".", None) => None,
(".", Some(field)) => Some(field),
(_, None) => Some(path),
(_, Some(field)) if key_in_path && (path == field || path.ends_with(&format!(".{field}"))) => Some(path),
(_, Some(field)) => Some(format!("{path}.{field}")),
}
}
pub fn read<T: DeserializeOwned>(kind: &'static str, file: &str, text: &str) -> Result<T, Refusal> {
ron::from_str(text).map_err(|e: ron::error::SpannedError| Refusal {
kind,
file: file.to_owned(),
field: field_of::<T>(text),
reason: e.to_string(),
})
}
pub fn read_lines<T: DeserializeOwned>(kind: &'static str, file: &str, text: &str) -> Result<Vec<T>, Refusal> {
let mut read = Vec::new();
let mut first: Option<(usize, Option<String>, String)> = None;
let mut damaged = 0usize;
for (n, line) in text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty()) {
match line_as::<T>(line) {
Ok(value) => read.push(value),
Err((field, reason)) => {
damaged += 1;
first.get_or_insert((n + 1, field, reason));
}
}
}
let Some((line, field, reason)) = first else { return Ok(read) };
let more = match damaged - 1 {
0 => String::new(),
1 => "; 1 more line does not read".to_owned(),
k => format!("; {k} more lines do not read"),
};
let field = match field {
Some(field) => format!("line {line}.{field}"),
None => format!("line {line}"),
};
Err(Refusal { kind, file: file.to_owned(), field: Some(field), reason: format!("{reason}{more}") })
}
fn line_as<T: DeserializeOwned>(line: &str) -> Result<T, (Option<String>, String)> {
fn said(e: &serde_json::Error) -> String {
match e.to_string().rsplit_once(" at line ") {
Some((what, _)) => format!("{what} (column {})", e.column()),
None => e.to_string(),
}
}
let mut de = serde_json::Deserializer::from_str(line);
match serde_path_to_error::deserialize::<_, T>(&mut de) {
Ok(value) => de.end().map(|()| value).map_err(|e| (None, said(&e))),
Err(err) => {
let path = err.path().to_string();
let message = err.inner().to_string();
let missing = message.strip_prefix("missing field `").and_then(|rest| rest.split_once('`')).map(|(f, _)| f.to_owned());
let field = match (path.as_str(), missing) {
(".", None) => None,
(".", Some(field)) => Some(field),
(_, None) => Some(path),
(_, Some(field)) => Some(format!("{path}.{field}")),
};
Err((field, said(err.inner())))
}
}
}
pub fn check(kind: &FileKind, file: &str, text: &str) -> Check {
use crate::{
composition::Compositions, data_files::DataFiles, family::Family, heightfield::Heightfield,
kits::Kits, library::Library, map::Map, note_agent::AgentSettings, notes::Notes, plan::Plan,
play::PlayConfig, policy::Policy, references::References, relations::Relations,
repetition::RepetitionRules, rigs::Rigs, rules::Rules, standards::Standards,
vocab::Vocabularies, volume::Volume, water::{WaterField, WaterPlan},
};
let result = match kind.id {
"kits" => run(text, Kits::parse),
"vocab" => run(text, Vocabularies::parse),
"library" => run(text, Library::parse),
"policy" => run(text, Policy::parse),
"rules" => run(text, Rules::parse),
"compositions" => run(text, Compositions::parse),
"standards" => run(text, Standards::parse),
"repetition" => run(text, RepetitionRules::parse),
"rigs" => run(text, Rigs::parse),
"notes" => run(text, Notes::from_ron),
"plan" => run(text, Plan::from_ron),
"references" => run(text, References::from_ron),
"relations" => run(text, Relations::parse),
"map" => run(text, Map::parse),
"terrain" => run(text, Heightfield::parse),
"water_plan" => run(text, WaterPlan::parse),
"water_field" => run(text, WaterField::parse),
"volume" => run(text, Volume::parse),
"data_registry" => run(text, DataFiles::parse),
"family_list" => run(text, Family::from_ron),
"agent" => run(text, AgentSettings::from_ron),
"play" => run(text, PlayConfig::from_ron),
_ => return Check::Unchecked,
};
match result {
Ok(()) => Check::Valid,
Err((field, reason)) => Check::Refused(Refusal { kind: kind.id, file: file.to_owned(), field, reason }),
}
}
pub fn unchecked() -> impl Iterator<Item = &'static FileKind> {
KINDS.iter().filter(|k| matches!(check(k, "", ""), Check::Unchecked))
}