aeruginous 2.2.0

The Aeruginous Open Source Development Toolbox.
Documentation
/*********************** GNU General Public License 3.0 ***********************\
|                                                                              |
|  Copyright (C) 2023 Kevin Matthes                                            |
|                                                                              |
|  This program is free software: you can redistribute it and/or modify        |
|  it under the terms of the GNU General Public License as published by        |
|  the Free Software Foundation, either version 3 of the License, or           |
|  (at your option) any later version.                                         |
|                                                                              |
|  This program is distributed in the hope that it will be useful,             |
|  but WITHOUT ANY WARRANTY; without even the implied warranty of              |
|  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               |
|  GNU General Public License for more details.                                |
|                                                                              |
|  You should have received a copy of the GNU General Public License           |
|  along with this program.  If not, see <https://www.gnu.org/licenses/>.      |
|                                                                              |
\******************************************************************************/

use crate::{
  Fragment, FromRon, PatternReader, PatternWriter, RonlogAction,
  RonlogReferences, RonlogSection, ToRon,
};
use std::path::PathBuf;
use sysexits::{ExitCode, Result};

#[derive(serde::Deserialize, serde::Serialize)]
struct Changelog {
  references: RonlogReferences,
  introduction: Option<String>,
  sections: Vec<RonlogSection>,
}

impl Changelog {
  fn add_section(&mut self, section: RonlogSection) {
    for s in &mut self.sections {
      if s == &section {
        s.merge(section);
        return;
      }
    }

    self
      .sections
      .insert(self.sections.partition_point(|s| s > &section), section);
  }

  fn init(
    path: &PathBuf,
    message: Option<String>,
    references: RonlogReferences,
    force: bool,
  ) -> Result<bool> {
    let result = !path.exists();

    if result || force {
      path.truncate(Box::new(Self::new(message, references).to_ron(2)?))?;
    }

    Ok(result)
  }

  #[must_use]
  const fn new(
    introduction: Option<String>,
    references: RonlogReferences,
  ) -> Self {
    Self {
      references,
      introduction,
      sections: Vec::new(),
    }
  }
}

struct Logic {
  cli: Ronlog,
  hyperlinks: RonlogReferences,
}

impl Logic {
  fn init(&self, message: Option<String>) -> Result<()> {
    if Changelog::init(
      &self.cli.output_file,
      message,
      self.hyperlinks.clone(),
      self.cli.force,
    )? {
      println!(
        "Successfully initialised new CHANGELOG in '{}'.",
        self.cli.output_file.display()
      );

      Ok(())
    } else if self.cli.force {
      println!(
        "Successfully re-initialised CHANGELOG in '{}'.",
        self.cli.output_file.display()
      );

      Ok(())
    } else {
      println!(
        "Use `--force` to overwrite the existing CHANGELOG in '{}'.",
        self.cli.output_file.display()
      );

      Err(ExitCode::Usage)
    }
  }

  fn main(&mut self) -> Result<()> {
    self.hyperlinks = self
      .cli
      .link
      .iter()
      .zip(self.cli.target.iter())
      .map(|(a, b)| (a.to_string(), b.to_string()))
      .collect();

    match self.cli.action {
      RonlogAction::Init => self.init(self.cli.message.clone()),
      RonlogAction::Release => self.release(),
    }
  }

  fn release(&self) -> Result<()> {
    if let Some(version) = &self.cli.version {
      let mut section = RonlogSection::new(
        Fragment::default(),
        version,
        self.cli.message.clone(),
        if self.hyperlinks.is_empty() {
          None
        } else {
          Some(self.hyperlinks.clone())
        },
      )?;

      if !self.cli.output_file.exists() {
        self.init(None)?;
      }

      for entry in std::fs::read_dir(&self.cli.input_directory)? {
        let entry = entry?.path();

        if entry
          .extension()
          .map_or(false, |e| e.to_str().map_or(false, |e| e == "ron"))
        {
          if let Ok(fragment) =
            Fragment::from_ron(&entry.read()?.try_into_string()?)
          {
            section.add_changes(fragment);
            std::fs::remove_file(entry)?;
          }
        }
      }

      let mut ronlog =
        Changelog::from_ron(&self.cli.output_file.read()?.try_into_string()?)?;

      for (link, target) in section.move_references() {
        ronlog
          .references
          .entry(link)
          .and_modify(|t| *t = target.clone())
          .or_insert(target);
      }

      ronlog.add_section(section);

      self.cli.output_file.truncate(Box::new(ronlog.to_ron(2)?))
    } else {
      eprintln!("No `--version` information provided for this mode.");
      Err(ExitCode::Usage)
    }
  }
}

/// Interact with RON CHANGELOGs.
#[derive(clap::Parser, Clone)]
pub struct Ronlog {
  /// The action on a certain RONLOG.
  action: RonlogAction,

  /// Whether to enforce this action.
  #[arg(long, short)]
  force: bool,

  /// The fragment storage to process.
  #[arg(default_value = ".", long = "input", short)]
  input_directory: String,

  /// A message to add as introduction.
  #[arg(long, short)]
  message: Option<String>,

  /// The hyperlinks to add.
  #[arg(aliases = ["hyperlink"], long, short)]
  link: Vec<String>,

  /// The RONLOG to modify.
  #[arg(default_value = "CHANGELOG.ron", long = "output", short)]
  output_file: PathBuf,

  /// The hyperlinks' targets.
  #[arg(long, short)]
  target: Vec<String>,

  /// The version to use.
  #[arg(long, short)]
  version: Option<String>,
}

impl Ronlog {
  /// Process the CLI instructions.
  ///
  /// # Errors
  ///
  /// See [`sysexits::ExitCode`].
  pub fn main(&self) -> Result<()> {
    self.wrap().main()
  }

  fn wrap(&self) -> Logic {
    Logic {
      cli: self.clone(),
      hyperlinks: RonlogReferences::new(),
    }
  }
}

/******************************************************************************/