diff --git a/src/blocks/compare/kinds.rs b/src/blocks/compare/kinds.rs new file mode 100644 index 0000000..e498545 --- /dev/null +++ b/src/blocks/compare/kinds.rs @@ -0,0 +1,103 @@ +use crate::blocks::intermediary::data::ModernBlockData; + +#[derive(Debug)] +pub enum PropertyComparison { + /// The base is equal to the target property -> compatible + Equal, + /// The base has less elements than the target property -> compatible + MissingEnd, + /// The base has more elements than the target property -> incompatible + LongerEnd, + /// The order of the base elements is different from the target property -> compatible + DifferentOrdering, + /// The values (and maybe ordering) of the base elements is different from the target property -> incompatible + DifferentValues, +} + +impl PropertyComparison { + pub fn compare<'raw>(base: &Vec<&'raw str>, target: &Vec<&'raw str>) -> Self { + if base == target { + Self::Equal + } else { + let mut last_index = 0; + for (i, value) in base.iter().enumerate() { + if matches!(target.get(i), Some(x) if x == value) { + if last_index != i { + break; + } + last_index += 1; + } + } + if last_index == base.len() || last_index == target.len() { + return if target.len() > base.len() { + Self::MissingEnd + } else { + Self::LongerEnd + } + } + for value in base { + if !target.contains(value) { + return Self::DifferentValues + } + } + Self::DifferentOrdering + } + } +} + +#[derive(Debug)] +pub enum BlockComparison { + /// The base properties equal to the target -> compatible + Equal, + /// The base has one or more different types from the target -> incompatible + DifferentType, + /// The base has a different enum type from the target -> maybe compatible + DifferentEnum, + /// The base has a different range from the target -> maybe compatible + DifferentRange, + /// The base has more properties than the target -> compatible + MoreProperties, + /// The base has less properties than the target -> incompatible + LessProperties, + /// If all else fails, this usually means the properties would be in a different order, + /// sometimes compatible, sometimes incompatible + DifferentOrder, +} + +impl BlockComparison { + pub fn compare<'raw>(base: &ModernBlockData<'raw>, target: &ModernBlockData<'raw>) -> Self { + if base.properties() == target.properties() { + Self::Equal + } else { + if let Some(properties) = base.properties() { + let target = if let Some(target) = target.properties() { + target + } else { + return Self::MoreProperties; + }; + for (name, kind) in properties { + if let Some(target) = target.get(name) { + if kind != target { + return if kind.is_enum() && target.is_enum() { + Self::DifferentEnum + } else if kind.is_range() && target.is_range() { + Self::DifferentRange + } else { + Self::DifferentType + } + } + } + } + if properties.len() < target.len() { + Self::LessProperties + } else if properties.len() > target.len() { + Self::MoreProperties + } else { + Self::DifferentOrder + } + } else { + Self::LessProperties + } + } + } +} diff --git a/src/blocks/compare/mod.rs b/src/blocks/compare/mod.rs new file mode 100644 index 0000000..9170dce --- /dev/null +++ b/src/blocks/compare/mod.rs @@ -0,0 +1,74 @@ +use anyhow::Context; +use clap::Args; +use crate::blocks::compare::kinds::{BlockComparison, PropertyComparison}; +use crate::blocks::intermediary::data::ModernBlockList; +use crate::util::file::InputFile; + +pub mod kinds; + +#[derive(Args, Debug)] +/// Compare two data versions one-directional +/// +/// Currently only compares block data +pub struct CompareCommand { + /// The intermediary data file to start from + base: InputFile, + /// The target intermediary data file to find compatibility with + target: InputFile, + /// Don't list missing entries + #[clap(long = "print-missing")] + print_missing: bool, +} + +impl CompareCommand { + pub fn compare<'raw>(&self) -> anyhow::Result<()> { + let base_version: ModernBlockList = self.base.data().with_context(|| format!("Error deserializing {:?}", self.base.name()))?; + let target_version: ModernBlockList = self.target.data().with_context(|| format!("Error deserializing {:?}", self.target.name()))?; + + eprintln!("Checking property compatibility...\n============="); + let mut missing_props = 0; + for (property, base) in base_version.properties() { + if let Some(target) = target_version.properties().get(property) { + match PropertyComparison::compare(base, target) { + PropertyComparison::Equal => {} + cmp => println!("\"{}\" has {:?}", property, cmp), + } + } else { + if self.print_missing { + println!("Missing property: \"{}\"!", property); + } + missing_props += 1; + } + } + eprintln!("=============\nChecking block compatibility...\n============="); + let mut missing_count = 0; + let mut same_base = 0; + let mut perfect_count = 0; + for (name, data) in base_version.blocks() { + if let Some(target) = target_version.blocks().get(name) { + match BlockComparison::compare(data, target) { + BlockComparison::Equal => { + if data.base_id() == target.base_id() { + perfect_count += 1; + } + } + cmp => eprintln!("\"{}\" has \"{:?}\"", name, cmp), + } + if data.base_id() == target.base_id() { + same_base += 1; + } + } else { + if self.print_missing { + println!("Missing blocks: \"{}\"", name); + } + missing_count += 1; + } + } + println!("=============\nMissing {} properties", missing_props); + println!("Missing {} blocks", missing_count); + println!("{} out of {} have the same base id", same_base, base_version.blocks().len() - missing_count); + println!("{} perfect blocks", perfect_count); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/blocks/intermediary/data.rs b/src/blocks/intermediary/data.rs index 26a7d85..91c0256 100644 --- a/src/blocks/intermediary/data.rs +++ b/src/blocks/intermediary/data.rs @@ -21,6 +21,14 @@ impl<'raw> ModernBlockList<'raw> { blocks, } } + + pub fn properties(&self) -> &LinkedHashMap<&'raw str, Vec<&'raw str>> { + &self.properties + } + + pub fn blocks(&self) -> &LinkedHashMap, ModernBlockData<'raw>, RandomState> { + &self.blocks + } } #[derive(Debug, Serialize, Deserialize)] @@ -41,6 +49,14 @@ impl<'raw> ModernBlockData<'raw> { default_id } } + + pub fn properties(&self) -> Option<&LinkedHashMap<&'raw str, TextOrRange<'raw>, RandomState>> { + self.kinds.as_ref() + } + + pub fn base_id(&self) -> i32 { + self.base_id + } } #[derive(Debug, Serialize, Deserialize)] @@ -59,4 +75,43 @@ impl<'raw> TextOrRange<'raw> { pub fn range(start: i32, end: i32) -> Self { Self::Range([start, end]) } + + pub fn is_bool(&self) -> bool { + matches!(self, TextOrRange::Text(val) if val == &"bool") + } + + pub fn is_enum(&self) -> bool { + matches!(self, TextOrRange::Text(val) if val != &"bool") + } + + pub fn is_range(&self) -> bool { + matches!(self, TextOrRange::Range(_)) + } + + pub fn get_enum(&self) -> Option<&'raw str> { + match self { + TextOrRange::Text(val) if val != &"bool" => { + Some(val) + } + _ => None + } + } +} + +impl<'raw> PartialEq for TextOrRange<'raw> { + fn eq(&self, other: &Self) -> bool { + match self { + TextOrRange::Text(val) => { + if let Self::Text(other) = other { + return val == other; + } + } + TextOrRange::Range(range) => { + if let Self::Range(other) = other { + return range == other; + } + } + } + false + } } \ No newline at end of file diff --git a/src/blocks/mod.rs b/src/blocks/mod.rs index 073ce9c..fe4c257 100644 --- a/src/blocks/mod.rs +++ b/src/blocks/mod.rs @@ -1,2 +1,3 @@ +pub mod compare; pub mod intermediary; pub mod raw; diff --git a/src/main.rs b/src/main.rs index d16eb88..5fa67e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use anyhow::Context; use clap::{Parser, Subcommand}; use blocks::intermediary::IntermediaryCommand; +use blocks::compare::CompareCommand; mod blocks; mod util; @@ -14,6 +15,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum SubCommands { + Compare(CompareCommand), Intermediary(IntermediaryCommand), } @@ -24,5 +26,8 @@ fn main() -> anyhow::Result<()> { SubCommands::Intermediary(cmd) => { cmd.generate_intermediate().with_context(|| "Error while generating data") } + SubCommands::Compare(cmd) => { + cmd.compare().with_context(|| "Error while comparing data") + } } }