usenetnews-dynexp2 0.1.2

USENET news server expiry time dynamic tuning (for INN2)
Documentation
// Copyright 2022 Ian Jackson
// SPDX-License-Identifier: GPL-3.0-or-later
// There is NO WARRANTY.

use crate::prelude::*;

impl FromStr for ExpireLine {
  type Err = AE;
  fn from_str(ls: &str) -> AR<Self> {
    (||{
      let mut words = ls.split(':');
      let mut next = || words.next().ok_or_else(|| anyhow!("too few fields"));
      let el = ExpireLine {
        pattern: next()?.into(),
        flag: next()?.into(),
        min: next()?.parse().context("min")?,
        def: next()?.parse().context("default")?,
        max: next()?.parse().context("max")?,
      };
      if words.next().is_some() {
        return Err(anyhow!("too many fields"));
      }
      Ok(el)
    })().context("active entry")
  }
}

#[derive(Debug, Clone)]
struct Setting {
  value: String,
  lno: usize,
}

type Settings = HashMap<String, Setting>;

/// Helper that processes `HashMap` into deserialise structs
///
/// Uses serde_ignored to report unknown config keys
pub struct SettingsConverter<'s> {
  filename: &'s str,
  settings: &'s Settings,
  unused: HashSet<&'s str>,
}

pub type SettingsUnusedReported = HashSet<String>;

impl<'s> SettingsConverter<'s> {
  fn new(filename: &'s str, settings: &'s Settings) -> Self {
    let unused = settings.keys().map(|s| &**s).collect();
    SettingsConverter {
      filename,
      settings,
      unused,
    }
  }

  fn check_all_used(mut self, dedup: &mut SettingsUnusedReported) -> AR<()> {
    for s in mem::take(&mut self.unused) {
      if dedup.insert(s.to_owned()) {
        let lno = &self.settings[s].lno;
        eprintln!("dynexp2: warning: {}:{}: unknown setting {:?}",
                  self.filename, lno, s);
      }
    }
    Ok(())
  }

  pub fn obtain1<T>(&mut self, kw: &str, def: Option<&dyn Fn() -> T>) -> AR<T>
  where T: FromStr + Clone, Result<T, T::Err>: anyhow::Context<T, T::Err>,
  {
    self.unused.remove(kw);

    let value = self.settings.get(kw)
      .map(|setting| {
        setting.value.parse()
          .with_context(|| format!("{}:{}", self.filename, setting.lno))
      })
      .transpose()?
      .or_else(|| def.map(|def| def()))
      .ok_or_else(|| anyhow!("missing setting {}", kw))?;
           
    Ok(value)
  }
}

impl ActiveData {
  pub fn new(params: Parameters, line: ExpireLine) -> AR<Self> {
    let maxdays = cmp::min(
      params.maxdays,
      line.max,
    ).try_into().map_err(|NeverError| anyhow!(
      "need expiry limit, but both maxdays (in parameters) and max (in expire line) are never"
    ))?;
    Ok(ActiveData {
      was: line.def,
      maxdays,
      params,
      line,
    })
  }
}

impl Loaded {
  pub fn read(filename: &str) -> AR<Self> {
    let f = File::open(filename)
      .with_context(|| filename.to_string())
      .context("open")?;
    Loaded::read_any(Box::new(f) as _, filename)
  }

  pub fn overwrite(&self, filename: &str) -> AR<()> {
    let tmp = format!("{}.tmp", filename);

    (||{
      let f = File::create(&tmp).context("create")?;
      let mut f = BufWriter::new(f);
      write!(f, "{}", self).context("write")?;
      f.flush().context("flush")?;
      AOk(())
    })()
      .with_context(|| tmp.to_string())
      .context("temporary output file")?;

    fs::rename(tmp, filename)
      .with_context(|| filename.to_string())
      .context("install new output file")?;

    Ok(())
  }

  pub fn read_any(f: Box<dyn io::Read>, filename: &str) -> AR<Self> {
    let f = BufReader::new(f);
    let mut settings = HashMap::new();
    let mut enable = false;
    let mut spools = BTreeMap::new();
    let mut lines = vec![];
    let mut dedup_unused = SettingsUnusedReported::default();

    for (lno, line_raw) in f.lines().enumerate() {
      let lno = lno + 1;
      let line_raw = line_raw
        .with_context(|| filename.to_string())
        .context("read")?;

      (||{
        let line = line_raw.trim();

        // Is it an actual expire instruction?
        if ! line.starts_with('#') && ! line.is_empty() && enable {

          // we must reify the parameters
          let mut settings = SettingsConverter::new(filename, &settings);
          let params = Parameters::from_settings(&mut settings)?;
          let spool_spec = SpoolSpec::from_settings(&mut settings)?;
          settings.check_all_used(&mut dedup_unused)?;
          params.check()?;

          let spool = spools.entry(spool_spec)
            .or_insert_with(|| SpoolEntries { entries: vec![] });
          
          let line: ExpireLine = line.parse()?;

          let ae = Rc::new(RefCell::new(ActiveData::new(params, line)?));
          spool.entries.push(ae.clone());
          lines.push(FileLine::Active(ae));
          return AOk(());
        }

        // Is it a magic comment?
        if let Some(mut words) = (||{
          let mut words = line.trim().split_ascii_whitespace();
          let mut expect_word = |w: &str| {
            if words.next()? != w { return None }
            Some(())
          };
          expect_word("#")?;
          expect_word("::")?;
          expect_word("dynexpire")?;
          Some(words)
        })() {
          let kw = words.next()
            .ok_or_else(|| anyhow!("keyword missing in setting"))?;

          match kw {
            "on" => enable = true,
            "off" => enable = false,
            "dffield" => { let _: Option<&str> = words.next(); },
            _ => {
              let value = words.next()
                .ok_or_else(|| anyhow!("value missing in setting"))?
                .into();
              settings.insert(kw.into(), Setting { value, lno });
            },
          }

          if words.next().is_some() {
            return Err(anyhow!("extraneous words in setting"))?;
          }

          // fall through, and treat it as inert then
        }

        lines.push(FileLine::Inert(line_raw));

        Ok(())
      })()
        .with_context(|| format!("{}:{}", &filename, lno))
        .context("parse error")?;
    }

    Ok(Loaded {
      lines,
      spools,
    })
  }
}

impl Display for Loaded {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    for l in &self.lines {
      match l {
        FileLine::Inert(s) => write!(f, "{}", s)?,
        FileLine::Active(ae) => write!(f, "{}", &ae.borrow().line)?,
      }
      writeln!(f)?;
    }
    Ok(())
  }
}

impl Display for ExpireLine {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}:{}", &self.pattern, &self.flag)?;
    for v in [
      &self.min as &dyn Display,
      &self.def as _,
      &self.max as _,
    ] {
      write!(f, ":{}", v)?;
    }
    Ok(())
  }
}