use crate::config_model::invar_config::*;
use crate::file_system::ConfigFormat;
use ahash::AHashMap;
use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::sync::LazyLock;
use toml::{Table, Value};
macro_rules! invar_struct {
($v:vis $name:ident $([$($derive:ident),+])? { $($x:ident : $y:ty),* $(,)? }) => {
#[derive(Deserialize,Serialize,Debug,Clone$($(,$derive)+)?)]
#[serde(rename_all = "kebab-case")]
$v struct $name {
write_mode: Option<WriteMode>,
executable: Option<bool>,
interpolate: Option<bool>,
props: Option<Table>,
$(
$x: $y
),*
}
};
}
invar_struct! [pub InvarConfigData[Default] {
target: Option<String>
}];
invar_struct! [pub InvarStateData[Default] {}];
static EMPTY_INVAR_CONFIG_DATA: LazyLock<InvarConfigData> = LazyLock::new(|| {
let default_invar_config_data = InvarConfigData::default();
InvarConfigData {
props: Some(Table::new()),
..default_invar_config_data
}
});
static EMPTY_INVAR_STATE_DATA: LazyLock<InvarStateData> = LazyLock::new(|| {
let default_invar_state_data = InvarStateData::default();
InvarStateData {
props: Some(Table::new()),
..default_invar_state_data
}
});
impl InvarConfigData {
pub fn new<IS: InvarState>(invar_state: &IS, target: Option<&String>) -> Self {
InvarConfigData {
write_mode: invar_state.write_mode_option(),
interpolate: invar_state.interpolate_option(),
executable: invar_state.executable_option(),
props: invar_state.props_option().clone(),
target: target.map(Clone::clone),
}
}
}
impl InvarStateData {
pub fn new() -> Self {
EMPTY_INVAR_STATE_DATA.clone()
}
}
#[cfg(test)]
mod test_invar_config_data {
use super::*;
#[test]
fn create_empty_invar_config_data() {
let empty_invar_state = InvarStateData::new();
let empty_invar_config_data = InvarConfigData::new(&empty_invar_state, None);
assert_eq!(empty_invar_config_data.write_mode, None);
assert_eq!(empty_invar_config_data.interpolate, None);
assert_eq!(empty_invar_config_data.props, Some(Table::new()));
}
}
impl InvarConfig for InvarConfigData {
fn from_str(body: &str, config_format: ConfigFormat) -> Result<Self> {
let invar_config: InvarConfigData = match config_format {
ConfigFormat::TOML => toml::from_str(body)?,
ConfigFormat::YAML => {
let result = serde_yaml::from_str(body)?;
#[cfg(test)]
crate::test_utils::log_toml("Invar Config", &result)?;
result
}
};
Ok(invar_config)
}
fn clone_state(&self) -> impl InvarState {
InvarStateData {
write_mode: self.write_mode.clone(),
executable: self.executable.clone(),
interpolate: self.interpolate.clone(),
props: self.props.clone(),
}
}
fn target(&self) -> Option<&String> {
self.target.as_ref()
}
}
impl InvarState for InvarStateData {
fn with_invar_state<'a, I: InvarState>(&'a self, invar_state: I) -> Cow<'a, Self> {
let dirty = false;
let (write_mode, dirty) =
merge_property(self.write_mode, invar_state.write_mode_option(), dirty);
debug!(
"Write mode: {:?} -> {:?} ({:?})",
self.write_mode, &write_mode, dirty
);
let (executable, dirty) =
merge_property(self.executable, invar_state.executable_option(), dirty);
debug!(
"Executable: {:?} -> {:?} ({:?})",
self.executable, &executable, dirty
);
let (interpolate, dirty) =
merge_property(self.interpolate, invar_state.interpolate_option(), dirty);
debug!(
"Interpolate: {:?} -> {:?} ({:?})",
self.interpolate, &interpolate, dirty
);
let (props, dirty) = merge_props(&self.props, &invar_state.props_option(), dirty);
debug!("Props ({:?})", dirty);
if dirty {
Cow::Owned(InvarStateData {
write_mode,
executable,
interpolate,
props: Some(props.into_owned()),
})
} else {
Cow::Borrowed(self)
}
}
fn with_write_mode_option<'a>(&'a self, write_mode: Option<WriteMode>) -> Cow<'a, Self> {
let invar_state = InvarStateData {
write_mode,
..EMPTY_INVAR_STATE_DATA.clone()
};
self.with_invar_state(invar_state)
}
fn with_write_mode<'a>(&'a self, write_mode: WriteMode) -> Cow<'a, Self> {
self.with_write_mode_option(Some(write_mode))
}
fn write_mode(&self) -> WriteMode {
self.write_mode.unwrap_or(WriteMode::Overwrite)
}
fn write_mode_option(&self) -> Option<WriteMode> {
self.write_mode
}
fn with_executable_option<'a>(&'a self, executable: Option<bool>) -> Cow<'a, Self> {
let invar_state = InvarStateData {
executable,
..EMPTY_INVAR_STATE_DATA.clone()
};
self.with_invar_state(invar_state)
}
fn with_executable<'a>(&'a self, executable: bool) -> Cow<'a, Self> {
self.with_executable_option(Some(executable))
}
fn executable(&self) -> bool {
self.executable.unwrap_or(false)
}
fn executable_option(&self) -> Option<bool> {
self.executable
}
fn with_interpolate_option<'a>(&'a self, interpolate: Option<bool>) -> Cow<'a, Self> {
let invar_state = InvarStateData {
interpolate,
..EMPTY_INVAR_STATE_DATA.clone()
};
self.with_invar_state(invar_state)
}
fn with_interpolate<'a>(&'a self, interpolate: bool) -> Cow<'a, Self> {
self.with_interpolate_option(Some(interpolate))
}
fn interpolate(&self) -> bool {
self.interpolate.unwrap_or(true)
}
fn interpolate_option(&self) -> Option<bool> {
self.interpolate
}
fn with_props_option<'a>(&'a self, props: Option<Table>) -> Cow<'a, Self> {
let invar_state = InvarStateData {
props,
..EMPTY_INVAR_STATE_DATA.clone()
};
self.with_invar_state(invar_state)
}
fn with_props<'a>(&'a self, props: Table) -> Cow<'a, Self> {
self.with_props_option(Some(props))
}
fn props<'a>(&'a self) -> Cow<'a, Table> {
self.props
.as_ref()
.map(Cow::Borrowed)
.unwrap_or(Cow::Owned(Table::new()))
}
fn props_option(&self) -> &Option<Table> {
&self.props
}
fn string_props(&self) -> AHashMap<String, String> {
to_string_map(self.props().as_ref())
}
}
fn merge_property<T: Copy + Eq>(
current_value_option: Option<T>,
new_value_option: Option<T>,
dirty: bool,
) -> (Option<T>, bool) {
match (current_value_option, new_value_option) {
(Some(current_value), Some(new_value)) => {
if new_value == current_value {
(current_value_option, dirty)
} else {
(new_value_option, true)
}
}
(None, Some(_)) => (new_value_option, true),
(_, _) => (current_value_option, dirty),
}
}
fn merge_props<'a>(
current_props_option: &'a Option<Table>,
new_props_option: &'a Option<Table>,
dirty: bool,
) -> (Cow<'a, Table>, bool) {
if let Some(current_props) = current_props_option {
if let Some(new_props) = new_props_option {
for (k, v) in new_props {
if current_props.get(k) != Some(v) {
let mut result = current_props.clone();
let new_props = new_props.clone();
result.extend(new_props);
return (Cow::Owned(result), true);
}
}
(Cow::Borrowed(current_props), dirty)
} else {
(Cow::Borrowed(current_props), dirty)
}
} else if let Some(new_props) = new_props_option {
(Cow::Borrowed(new_props), true)
} else {
(Cow::Owned(Table::new()), true)
}
}
fn to_string_map(props: &Table) -> AHashMap<String, String> {
props
.iter()
.map(to_strings)
.filter(Option::is_some)
.map(Option::unwrap)
.collect()
}
fn to_strings(entry: (&String, &Value)) -> Option<(String, String)> {
if let (key, Value::String(val)) = entry {
Some((key.to_owned(), val.to_owned()))
} else {
None
}
}
#[cfg(test)]
mod test {
use super::super::serde_test_utils::insert_entry;
use super::*;
use crate::config_model::WriteMode::*;
use test_log::test;
#[test]
fn with_write_mode_from_none_to_something() {
let invar_state = new_invar_state();
assert_eq!(invar_state.write_mode_option(), None);
let updated = invar_state.with_write_mode(Overwrite);
assert_owned(&updated);
assert_eq!(updated.write_mode_option(), Some(Overwrite));
}
#[test]
fn with_write_mode_from_none_to_some_thing() {
let invar_state = new_invar_state();
assert_eq!(invar_state.write_mode_option(), None);
let updated = invar_state.with_write_mode_option(Some(Overwrite));
assert_owned(&updated);
assert_eq!(updated.write_mode_option(), Some(Overwrite));
}
#[test]
fn with_write_mode_from_none_to_none() {
let invar_state = new_invar_state();
assert_eq!(invar_state.write_mode_option(), None);
let updated = invar_state.with_write_mode_option(None);
assert_borrowed(&updated);
assert_eq!(updated.write_mode_option(), None);
}
#[test]
fn with_write_mode_from_something_to_something_same() {
let invar_state = new_invar_state().with_write_mode(Ignore).into_owned();
assert_eq!(invar_state.write_mode_option(), Some(Ignore));
let updated = invar_state.with_write_mode(Ignore);
assert_borrowed(&updated);
assert_eq!(updated.write_mode_option(), Some(Ignore));
}
#[test]
fn with_write_mode_from_something_to_some_thing_same() {
let invar_state = new_invar_state().with_write_mode(Ignore).into_owned();
assert_eq!(invar_state.write_mode_option(), Some(Ignore));
let updated = invar_state.with_write_mode_option(Some(Ignore));
assert_borrowed(&updated);
assert_eq!(updated.write_mode_option(), Some(Ignore));
}
#[test]
fn with_write_mode_from_something_to_something_different() {
let invar_state = new_invar_state().with_write_mode(Ignore).into_owned();
assert_eq!(invar_state.write_mode_option(), Some(Ignore));
let updated = invar_state.with_write_mode(Overwrite);
assert_owned(&updated);
assert_eq!(updated.write_mode_option(), Some(Overwrite));
}
#[test]
fn with_write_mode_from_something_to_some_thing_different() {
let invar_state = new_invar_state().with_write_mode(Ignore).into_owned();
assert_eq!(invar_state.write_mode_option(), Some(Ignore));
let updated = invar_state.with_write_mode_option(Some(Overwrite));
assert_owned(&updated);
assert_eq!(updated.write_mode_option(), Some(Overwrite));
}
#[test]
fn with_write_mode_from_something_to_none() {
let invar_state = new_invar_state().with_write_mode(Ignore).into_owned();
assert_eq!(invar_state.write_mode_option(), Some(Ignore));
let updated = invar_state.with_write_mode_option(None);
assert_borrowed(&updated);
assert_eq!(updated.write_mode_option(), Some(Ignore)); }
#[test]
fn with_interpolate_from_none_to_something() {
let invar_state = new_invar_state();
assert_eq!(invar_state.interpolate_option(), None);
let updated = invar_state.with_interpolate(false);
assert_owned(&updated);
assert_eq!(updated.interpolate_option(), Some(false));
}
#[test]
fn with_interpolate_from_none_to_some_thing() {
let invar_state = new_invar_state();
assert_eq!(invar_state.interpolate_option(), None);
let updated = invar_state.with_interpolate_option(Some(false));
assert_owned(&updated);
assert_eq!(updated.interpolate_option(), Some(false));
}
#[test]
fn with_interpolate_from_none_to_none() {
let invar_state = new_invar_state();
assert_eq!(invar_state.interpolate_option(), None);
let updated = invar_state.with_interpolate_option(None);
assert_borrowed(&updated);
assert_eq!(updated.interpolate_option(), None);
}
#[test]
fn with_interpolate_from_something_to_something_same() {
let invar_state = new_invar_state().with_interpolate(false).into_owned();
assert_eq!(invar_state.interpolate_option(), Some(false));
let updated = invar_state.with_interpolate(false);
assert_borrowed(&updated);
assert_eq!(updated.interpolate_option(), Some(false));
}
#[test]
fn with_interpolate_from_something_to_some_thing_same() {
let invar_state = new_invar_state().with_interpolate(false).into_owned();
assert_eq!(invar_state.interpolate_option(), Some(false));
let updated = invar_state.with_interpolate_option(Some(false));
assert_borrowed(&updated);
assert_eq!(updated.interpolate_option(), Some(false));
}
#[test]
fn with_interpolate_from_something_to_something_different() {
let invar_state = new_invar_state().with_interpolate(false).into_owned();
assert_eq!(invar_state.interpolate_option(), Some(false));
let updated = invar_state.with_interpolate(true);
assert_owned(&updated);
assert_eq!(updated.interpolate_option(), Some(true));
}
#[test]
fn with_interpolate_from_something_to_some_thing_different() {
let invar_state = new_invar_state().with_interpolate(false).into_owned();
assert_eq!(invar_state.interpolate_option(), Some(false));
let updated = invar_state.with_interpolate_option(Some(true));
assert_owned(&updated);
assert_eq!(updated.interpolate_option(), Some(true));
}
#[test]
fn with_interpolate_from_something_to_none() {
let invar_state = new_invar_state().with_interpolate(false).into_owned();
assert_eq!(invar_state.interpolate_option(), Some(false));
let updated = invar_state.with_interpolate_option(None);
assert_borrowed(&updated);
assert_eq!(updated.interpolate_option(), Some(false)); }
#[test]
fn with_props_from_none_to_something() {
let invar_state = new_invar_state();
let mut mapping = Table::new();
insert_entry(&mut mapping, "foo", "bar");
let updated = invar_state.with_props(mapping.clone());
assert_owned(&updated);
assert_eq!(updated.props_option(), &Some(mapping));
}
#[test]
fn with_props_from_none_to_some_thing() {
let invar_state = new_invar_state();
let mut mapping = Table::new();
insert_entry(&mut mapping, "foo", "bar");
let updated = invar_state.with_props_option(Some(mapping.clone()));
assert_owned(&updated);
assert_eq!(updated.props_option(), &Some(mapping));
}
#[test]
fn with_props_from_none_to_none() {
let invar_state = new_invar_state();
let updated = invar_state.with_props_option(None);
assert_borrowed(&updated);
assert_eq!(updated.props_option(), &Some(Table::new()));
}
#[test]
fn with_props_from_something_add_same() {
let mut old_mapping = Table::new();
insert_entry(&mut old_mapping, "foo", "bar");
insert_entry(&mut old_mapping, "food", "baz");
let old_mapping = old_mapping; let invar_config = new_invar_state()
.with_props(old_mapping.clone())
.into_owned();
let mut new_mapping = Table::new();
insert_entry(&mut new_mapping, "foo", "bar");
let updated = invar_config.with_props(new_mapping.clone());
assert_borrowed(&updated);
assert_eq!(updated.props_option(), &Some(old_mapping));
}
#[test]
fn with_props_from_something_add_different() {
let mut old_mapping = Table::new();
insert_entry(&mut old_mapping, "foo", "bar");
insert_entry(&mut old_mapping, "food", "baz");
let old_mapping = old_mapping; let invar_state = new_invar_state()
.with_props(old_mapping.clone())
.into_owned();
let mut new_mapping = Table::new();
insert_entry(&mut new_mapping, "foo", "beep");
let updated = invar_state.with_props(new_mapping.clone());
let mut updated_mapping = old_mapping.clone();
assert_owned(&updated);
insert_entry(&mut updated_mapping, "foo", "beep");
assert_eq!(updated.props_option(), &Some(updated_mapping));
}
#[test]
fn with_props_from_something_add_new() {
let mut old_mapping = Table::new();
insert_entry(&mut old_mapping, "foo", "bar");
insert_entry(&mut old_mapping, "food", "baz");
let old_mapping = old_mapping; let invar_state = new_invar_state()
.with_props(old_mapping.clone())
.into_owned();
let mut new_mapping = Table::new();
insert_entry(&mut new_mapping, "oh", "joy");
let updated = invar_state.with_props(new_mapping.clone());
let mut updated_mapping = old_mapping.clone();
assert_owned(&updated);
insert_entry(&mut updated_mapping, "oh", "joy");
assert_eq!(updated.props_option(), &Some(updated_mapping));
}
#[test]
fn string_props() {
let mut mapping = Table::new();
insert_entry(&mut mapping, "foo", "bar");
insert_entry(&mut mapping, "food", "baz");
let invar_state = new_invar_state().with_props(mapping).into_owned();
let string_props = invar_state.string_props();
let mut expected = AHashMap::new();
expected.insert("foo".to_string(), "bar".to_string());
expected.insert("food".to_string(), "baz".to_string());
assert_eq!(string_props, expected);
}
fn new_invar_state() -> impl InvarState {
InvarStateData::new()
}
fn assert_owned(invar_config: &Cow<impl InvarState>) {
if let Cow::Owned(_) = invar_config {
return;
} else {
assert_eq!("borrowed", "owned")
}
}
fn assert_borrowed(invar_config: &Cow<impl InvarState>) {
if let Cow::Borrowed(_) = invar_config {
return;
} else {
assert_eq!("owned", "borrowed")
}
}
}