use crate::{OutputFolder, OutputFolderKey, table::Table};
use anyhow::{Context as _, Result, ensure};
#[derive(Clone, Debug)]
pub struct TableMassEditLineData {
web_url: String,
output: OutputFolderKey,
name: Option<String>,
symbol: Option<String>,
}
impl std::fmt::Display for TableMassEditLineData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}{}{}{}{}{}{}",
self.web_url,
Self::SEPARATOR,
self.output,
Self::SEPARATOR,
self.name.as_deref().unwrap_or(Self::DEFAULT_SYMBOL),
Self::SEPARATOR,
self.symbol.as_deref().unwrap_or(Self::DEFAULT_SYMBOL),
))
}
}
impl TableMassEditLineData {
const DEFAULT_SYMBOL: &str = "DEFAULT";
const SEPARATOR: &str = " ";
fn try_from_table(table: &Table) -> Result<Self> {
let out = Self {
web_url: table.0.web_url.as_str().to_string(),
output: table.1.output.clone(),
name: table.1.user_name.clone(),
symbol: table.1.user_symbol.clone(),
};
ensure!(
!out.web_url.contains(Self::SEPARATOR) && !out.web_url.contains('\n'),
"Table URL '{}' must not contain separator or newline",
out.web_url
);
ensure!(
!out.output.0.contains(Self::SEPARATOR) && !out.output.0.contains('\n'),
"Table output '{}' must not contain separator or newline",
out.output.0
);
if let Some(name) = out.name.as_ref() {
ensure!(
!name.contains(Self::SEPARATOR) && !name.contains('\n'),
"Table name '{name}' must not contain separator or newline",
);
}
if let Some(symbol) = out.symbol.as_ref() {
ensure!(
!symbol.contains(Self::SEPARATOR) && !symbol.contains('\n'),
"Table symbol '{symbol}' must not contain separator or newline",
);
}
Ok(out)
}
fn try_from_string(s: &str) -> Result<Self> {
let split = s.split(Self::SEPARATOR).collect::<Vec<&str>>();
ensure!(split.len() == 4, "invalid entity count in string: {s}");
Ok(Self {
web_url: split[0].to_string(),
output: OutputFolderKey(split[1].to_string()),
name: (split[2] != Self::DEFAULT_SYMBOL).then(|| split[2].to_string()),
symbol: (split[3] != Self::DEFAULT_SYMBOL).then(|| split[3].to_string()),
})
}
}
pub fn with_mass_edit_changes(
old_tables: &[Table],
edits: Vec<TableMassEditLineData>,
) -> Result<Vec<Table>> {
let mut tables = old_tables.to_vec();
for t in &mut *tables {
if !edits
.iter()
.any(|line| line.web_url == t.0.web_url.as_str())
{
t.1.pending_removal = true;
}
}
for edit in edits {
match tables
.iter_mut()
.find(|t| t.0.web_url.as_str() == edit.web_url)
{
Some(t) => {
t.1.output = edit.output;
t.1.user_name = edit.name;
t.1.user_symbol = edit.symbol;
}
None => Table::insert_new(&mut tables, edit.web_url.try_into()?, edit.output)?,
}
}
Ok(tables)
}
pub fn parse_mass_edit_lines(s: &str) -> Result<Vec<TableMassEditLineData>> {
s.split(['\r', '\n'])
.filter(|s| !s.is_empty())
.filter(|s| !s.starts_with('#'))
.map(TableMassEditLineData::try_from_string)
.collect::<Result<Vec<_>>>()
.context("failed to parse mass edit lines")
}
pub fn validate_mass_edits(
edits: &[TableMassEditLineData],
outputs: &[OutputFolder],
) -> Result<()> {
for edit in edits {
ensure!(
outputs.iter().any(|o| o.0 == edit.output),
"edit of '{}' references undefined output '{}'",
edit.web_url,
edit.output.0
);
}
Ok(())
}
pub fn to_mass_edit_lines(tables: &[Table]) -> Result<String> {
let mut lines = Vec::<String>::with_capacity(tables.len() + 1);
lines.push(
"# ' ' delimited.\n# URL OUTPUT NAME ('DEFAULT' for none) SYMBOL ('DEFAULT' for none)".to_string(),
);
for entry in tables {
lines.push(format!("# {} ({})", entry.0.name, entry.0.symbol));
lines.push(
TableMassEditLineData::try_from_table(entry)
.context("Failed to convert table to mass edit line")?
.to_string(),
);
}
Ok(lines.as_slice().join("\n"))
}
#[cfg(test)]
mod tests {
use test_log::test;
#[test]
fn to_mass_edit_lines() {
use super::to_mass_edit_lines as f;
use crate::table::Table;
assert_eq!(
f(&[Table::empty().with_url("https://?a= &b=")])
.unwrap_err()
.to_string(),
"Failed to convert table to mass edit line"
);
assert_eq!(
f(&[Table::empty().with_output(" ")])
.unwrap_err()
.to_string(),
"Failed to convert table to mass edit line"
);
assert_eq!(
f(&[Table::empty().with_user_symbol(" ")])
.unwrap_err()
.to_string(),
"Failed to convert table to mass edit line"
);
assert_eq!(
f(&[]).unwrap(),
"# ' ' delimited.\n# URL OUTPUT NAME ('DEFAULT' for none) SYMBOL ('DEFAULT' for none)"
);
assert_eq!(
f(&[
Table::empty()
.with_name("name")
.with_user_name("user-name")
.with_url("https://url")
.with_symbol("symbol")
.with_user_symbol("user-symbol")
.with_output("output"),
Table::empty().with_url("https://url"),
])
.unwrap(),
"# ' ' delimited.
# URL OUTPUT NAME ('DEFAULT' for none) SYMBOL ('DEFAULT' for none)
# name (symbol)
https://url output user-name user-symbol
# ()
https://url DEFAULT DEFAULT"
);
}
#[test]
fn parse_mass_edit_lines() {
use super::{TableMassEditLineData, parse_mass_edit_lines as f};
let to_str =
|t: &[TableMassEditLineData]| t.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_eq!(
to_str(
&f("# comment line ignored\r\nbad-protocol:// output DEFAULT DEFAULT")
.unwrap()
),
["bad-protocol:// output DEFAULT DEFAULT"]
);
assert_eq!(
&f("bad").unwrap_err().to_string(),
"failed to parse mass edit lines"
);
assert_eq!(
&f(". . . . excessive-field")
.unwrap_err()
.to_string(),
"failed to parse mass edit lines"
);
}
#[test]
fn validate_mass_edits() {
use super::{TableMassEditLineData, validate_mass_edits as f};
use crate::{OutputFolder, OutputFolderKey};
f(&[], &[]).unwrap();
f(
&[TableMassEditLineData {
web_url: "http://".to_string(),
output: OutputFolderKey("some".into()),
name: None,
symbol: None,
}],
&[OutputFolder(OutputFolderKey("some".into()), "/tmp".into())],
)
.unwrap();
assert_eq!(
f(
&[TableMassEditLineData {
web_url: "http://".to_string(),
output: OutputFolderKey("some".into()),
name: None,
symbol: None,
}],
&[OutputFolder(
OutputFolderKey("another".into()),
"/tmp".into(),
)],
)
.unwrap_err()
.to_string(),
"edit of 'http://' references undefined output 'some'"
);
}
#[test]
fn with_mass_edit_changes() {
use super::{TableMassEditLineData, with_mass_edit_changes as f};
use crate::{OutputFolderKey, table::Table};
assert_eq!(
f(
&[],
vec![TableMassEditLineData {
web_url: "bad-url".to_string(),
output: OutputFolderKey(String::new()),
name: None,
symbol: None,
}]
)
.unwrap_err()
.to_string(),
"Invalid protocol in URL: bad-url"
);
{
let mut tables = f(
&[
Table::empty().with_url("http://1").with_name("1"),
Table::empty()
.with_url("http://2")
.with_name("2")
.with_user_symbol("old"),
Table::empty().with_url("http://3").with_name("3"),
],
vec![
TableMassEditLineData {
web_url: "http://2".to_string(),
output: OutputFolderKey("out-for-2".into()),
name: Some("new".to_string()),
symbol: Some("new".to_string()),
},
TableMassEditLineData {
web_url: "http://3".to_string(),
output: OutputFolderKey("out-for-3".into()),
name: None,
symbol: None,
},
TableMassEditLineData {
web_url: "http://4".to_string(),
output: OutputFolderKey("out-for-4".into()),
name: None,
symbol: None,
},
],
)
.unwrap();
Table::commit_removals(&mut tables);
assert_eq!(
tables
.iter()
.map(|t| format!(
"{} {} {} {}",
t.0.web_url.as_str(),
t.1.output.0,
t.1.user_name.as_deref().unwrap_or("nil"),
t.1.user_symbol.as_deref().unwrap_or("nil")
))
.collect::<Vec<_>>(),
[
"http://2 out-for-2 new new",
"http://3 out-for-3 nil nil",
"http://4 out-for-4 nil nil"
]
);
}
}
}