use crate::game::eu4::content::ScriptFileKind;
use crate::game::eu4::content::eu4;
use crate::game::eu4::script::ParsedScriptFile;
use crate::game::eu4::script::parser::{AstFile, AstStatement};
use crate::model::{GamePathBuf, Isolation, ParseIssue, SourceRepair};
use std::path::Path;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct StoredParsedScriptFile {
mod_id: String,
path: String,
relative_path: GamePathBuf,
file_kind: ScriptFileKind,
module_name: String,
ast: StoredAstFile,
source: String,
parse_issues: Vec<StoredParseIssue>,
parse_cache_hit: bool,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct StoredAstFile {
path: String,
statements: Vec<AstStatement>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct StoredParseIssue {
mod_id: String,
path: GamePathBuf,
line: usize,
column: usize,
message: String,
repair: Option<SourceRepair>,
isolation: Option<Isolation>,
}
pub(crate) fn encode_parsed_documents(documents: &[ParsedScriptFile]) -> Result<Vec<u8>, String> {
let stored = documents
.iter()
.map(StoredParsedScriptFile::from_parsed_script_file)
.collect::<Vec<_>>();
bincode::serialize(&stored).map_err(|err| err.to_string())
}
pub(crate) fn decode_parsed_documents(
bytes: &[u8],
root: &Path,
) -> Result<Vec<ParsedScriptFile>, String> {
let stored = bincode::deserialize::<Vec<StoredParsedScriptFile>>(bytes)
.map_err(|err| err.to_string())?;
Ok(stored
.into_iter()
.map(|file| file.into_parsed_script_file(root))
.collect())
}
impl StoredParsedScriptFile {
fn from_parsed_script_file(file: &ParsedScriptFile) -> Self {
Self {
mod_id: file.mod_id.clone(),
path: String::new(),
relative_path: file.relative_path.clone(),
file_kind: file.file_kind.clone(),
module_name: file.module_name.clone(),
ast: StoredAstFile::from_ast_file(&file.ast),
source: file.source.clone(),
parse_issues: file
.parse_issues
.iter()
.map(StoredParseIssue::from_parse_issue)
.collect(),
parse_cache_hit: true,
}
}
fn into_parsed_script_file(self, root: &Path) -> ParsedScriptFile {
let content_family = eu4().classify_content_family(&self.relative_path);
let ast = AstFile {
path: self.relative_path.clone(),
statements: self.ast.statements,
};
ParsedScriptFile {
mod_id: self.mod_id,
path: Some(self.relative_path.to_path(root)),
relative_path: self.relative_path,
content_family,
file_kind: self.file_kind,
module_name: self.module_name,
ast,
source: self.source,
parse_issues: self
.parse_issues
.into_iter()
.map(StoredParseIssue::into_parse_issue)
.collect(),
parse_cache_hit: true,
}
}
}
impl StoredAstFile {
fn from_ast_file(file: &AstFile) -> Self {
Self {
path: file.path.as_str().to_owned(),
statements: file.statements.clone(),
}
}
}
impl StoredParseIssue {
fn from_parse_issue(item: &ParseIssue) -> Self {
Self {
mod_id: item.mod_id.clone(),
path: item.path.clone(),
line: item.line,
column: item.column,
message: item.message.clone(),
repair: item.repair,
isolation: item.isolation.clone(),
}
}
fn into_parse_issue(self) -> ParseIssue {
ParseIssue {
mod_id: self.mod_id,
path: self.path,
line: self.line,
column: self.column,
message: self.message,
repair: self.repair,
isolation: self.isolation,
}
}
}
#[cfg(test)]
pub(super) mod text_layout {
use crate::game::eu4::content::ScriptFileKind;
use crate::game::eu4::script::ParsedScriptFile;
use crate::game::eu4::script::parser::AstStatement;
use crate::model::{Isolation, SourceRepair};
#[derive(Clone, Debug, serde::Serialize)]
pub struct File {
pub mod_id: String,
pub path: String,
pub relative_path: String,
pub file_kind: ScriptFileKind,
pub module_name: String,
pub ast: Ast,
pub source: String,
pub parse_issues: Vec<Issue>,
pub parse_cache_hit: bool,
}
#[derive(Clone, Debug, serde::Serialize)]
pub struct Ast {
pub path: String,
pub statements: Vec<AstStatement>,
}
#[derive(Clone, Debug, serde::Serialize)]
pub struct Issue {
pub mod_id: String,
pub path: String,
pub line: usize,
pub column: usize,
pub message: String,
pub repair: Option<SourceRepair>,
pub isolation: Option<Isolation>,
}
impl File {
pub fn of(file: &ParsedScriptFile) -> Self {
Self {
mod_id: file.mod_id.clone(),
path: String::new(),
relative_path: file.relative_path.as_str().to_owned(),
file_kind: file.file_kind.clone(),
module_name: file.module_name.clone(),
ast: Ast {
path: file.ast.path.as_str().to_owned(),
statements: file.ast.statements.clone(),
},
source: file.source.clone(),
parse_issues: file
.parse_issues
.iter()
.map(|issue| Issue {
mod_id: issue.mod_id.clone(),
path: issue.path.as_str().to_owned(),
line: issue.line,
column: issue.column,
message: issue.message.clone(),
repair: issue.repair,
isolation: issue.isolation.clone(),
})
.collect(),
parse_cache_hit: true,
}
}
}
pub fn encode(files: &[File]) -> Vec<u8> {
bincode::serialize(files).expect("encode text-layout parsed scripts")
}
}
#[cfg(test)]
mod tests {
use super::text_layout;
use super::*;
use crate::game::eu4::script::parse_script_bytes_cached;
use crate::model::GamePath;
fn parsed_with_issue(relative: &str) -> ParsedScriptFile {
let relative = GamePath::new(relative).expect("valid game path");
let file = parse_script_bytes_cached(
"__game__eu4",
Path::new("/build"),
relative,
b"= 1\na = 1\n",
);
assert!(
!file.parse_issues.is_empty(),
"the fixture must carry a parse issue"
);
file
}
#[test]
fn decoded_documents_are_located_under_the_reading_root() {
let root = Path::new("/reader/game");
let encoded =
encode_parsed_documents(&[parsed_with_issue("events/a.txt")]).expect("encode");
let decoded = decode_parsed_documents(&encoded, root).expect("decode");
let [document] = decoded.as_slice() else {
panic!("expected one document");
};
assert_eq!(document.relative_path.as_str(), "events/a.txt");
assert_eq!(document.ast.path, document.relative_path);
assert_eq!(document.parse_issues[0].path, document.relative_path);
assert_eq!(
document.path.as_deref(),
Some(root.join("events").join("a.txt").as_path())
);
assert!(document.parse_cache_hit);
}
#[test]
fn the_section_keeps_the_text_layout() {
let file = parsed_with_issue("events/a.txt");
assert_eq!(
encode_parsed_documents(std::slice::from_ref(&file)).expect("encode"),
text_layout::encode(&[text_layout::File::of(&file)])
);
}
#[test]
fn a_snapshot_that_recorded_the_builders_files_still_decodes() {
let builder_file = "/builder/machine/Europa Universalis IV/events/a.txt";
let mut legacy = text_layout::File::of(&parsed_with_issue("events/a.txt"));
legacy.path = builder_file.to_string();
legacy.ast.path = builder_file.to_string();
let root = Path::new("/reader/game");
let decoded = decode_parsed_documents(&text_layout::encode(&[legacy]), root)
.expect("a legacy section decodes");
let [document] = decoded.as_slice() else {
panic!("expected one document");
};
assert_eq!(document.ast.path.as_str(), "events/a.txt");
assert_eq!(
document.path.as_deref(),
Some(root.join("events").join("a.txt").as_path())
);
}
#[test]
fn each_read_game_path_that_is_not_valid_fails_to_decode() {
let invalid = r"events\a.txt";
let valid = text_layout::File::of(&parsed_with_issue("events/a.txt"));
let mut relative_path = valid.clone();
relative_path.relative_path = invalid.to_string();
let mut issue_path = valid;
issue_path.parse_issues[0].path = invalid.to_string();
for (field, file) in [
("relative_path", relative_path),
("parse issue path", issue_path),
] {
let error =
decode_parsed_documents(&text_layout::encode(&[file]), Path::new("/reader/game"))
.expect_err(field);
assert!(
error.contains(&format!("invalid game path `{invalid}`")),
"{field}: {error}"
);
}
}
}