link-cli 0.2.6

A CLI tool and reusable library for links manipulation backed by a LiNo-notation doublet storage engine.
Documentation
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;

use crate::error::LinkError;
use crate::link::Link;
use crate::link_storage::LinkStorage;
use crate::named_types::{NamedTypes, NamedTypesDecorator};

pub trait NamedTypeLinks {
    fn create(&mut self, source: u32, target: u32) -> u32;
    fn ensure_created(&mut self, id: u32) -> u32;
    fn try_ensure_created(&mut self, id: u32) -> Result<u32> {
        if id == 0 || id == u32::MAX {
            return Err(LinkError::InvalidFormat(format!(
                "Cannot ensure unsupported link address {id}"
            ))
            .into());
        }

        Ok(self.ensure_created(id))
    }
    fn get_link(&mut self, id: u32) -> Option<Link>;
    fn exists(&mut self, id: u32) -> bool;
    fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link>;
    fn delete(&mut self, id: u32) -> Result<Link>;
    fn all_links(&mut self) -> Vec<Link>;
    fn search(&mut self, source: u32, target: u32) -> Option<u32>;
    fn get_or_create(&mut self, source: u32, target: u32) -> u32;
    fn get_name(&mut self, id: u32) -> Result<Option<String>>;
    fn set_name(&mut self, id: u32, name: &str) -> Result<u32>;
    fn get_by_name(&mut self, name: &str) -> Result<Option<u32>>;
    fn remove_name(&mut self, id: u32) -> Result<()>;
    fn save(&mut self) -> Result<()>;

    fn get_or_create_named(&mut self, name: &str) -> Result<u32> {
        if let Some(id) = self.get_by_name(name)? {
            return Ok(id);
        }

        let id = self.create(0, 0);
        self.set_name(id, name)?;
        self.update(id, id, id)?;
        Ok(id)
    }

    fn format_reference(&mut self, id: u32) -> Result<String> {
        Ok(self
            .get_name(id)?
            .map(|name| escape_lino_reference(&name))
            .unwrap_or_else(|| id.to_string()))
    }

    fn format_lino(&mut self, link: &Link) -> Result<String> {
        Ok(format!(
            "({}: {} {})",
            self.format_reference(link.index)?,
            self.format_reference(link.source)?,
            self.format_reference(link.target)?
        ))
    }

    fn lino_lines(&mut self) -> Result<Vec<String>> {
        let mut links = self.all_links();
        links.sort_by_key(|link| link.index);

        links
            .iter()
            .map(|link| self.format_lino(link))
            .collect::<Result<Vec<_>>>()
    }

    fn write_lino_output<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
        let path = path.as_ref();
        let file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)
            .with_context(|| format!("Failed to create LiNo output: {}", path.display()))?;

        let mut writer = BufWriter::new(file);
        for line in self.lino_lines()? {
            writeln!(writer, "{line}")?;
        }
        writer.flush()?;
        Ok(())
    }

    fn print_all_lino(&mut self) -> Result<()> {
        for line in self.lino_lines()? {
            println!("{line}");
        }
        Ok(())
    }

    fn print_change_lino(&mut self, before: &Option<Link>, after: &Option<Link>) -> Result<()> {
        let before_text = before
            .map(|link| self.format_lino(&link))
            .transpose()?
            .unwrap_or_default();
        let after_text = after
            .map(|link| self.format_lino(&link))
            .transpose()?
            .unwrap_or_default();
        println!("({before_text}) ({after_text})");
        Ok(())
    }

    fn format_structure(&mut self, id: u32) -> Result<String> {
        let mut visited = HashSet::new();
        self.format_structure_recursive(id, &mut visited)
    }

    fn format_structure_recursive(
        &mut self,
        id: u32,
        visited: &mut HashSet<u32>,
    ) -> Result<String> {
        let link = self.get_link(id).ok_or(LinkError::NotFound(id))?;
        if !visited.insert(id) {
            return self.format_reference(id);
        }

        let source = if self.exists(link.source) && !visited.contains(&link.source) {
            self.format_structure_recursive(link.source, visited)?
        } else {
            self.format_reference(link.source)?
        };
        let target = self.format_reference(link.target)?;
        let index = self.format_reference(link.index)?;
        visited.remove(&id);

        Ok(format!("({index}: {source} {target})"))
    }
}

impl NamedTypeLinks for LinkStorage {
    fn create(&mut self, source: u32, target: u32) -> u32 {
        LinkStorage::create(self, source, target)
    }

    fn ensure_created(&mut self, id: u32) -> u32 {
        LinkStorage::ensure_created(self, id)
    }

    fn get_link(&mut self, id: u32) -> Option<Link> {
        self.get(id).copied()
    }

    fn exists(&mut self, id: u32) -> bool {
        LinkStorage::exists(self, id)
    }

    fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
        LinkStorage::update(self, id, source, target)
    }

    fn delete(&mut self, id: u32) -> Result<Link> {
        LinkStorage::delete(self, id)
    }

    fn all_links(&mut self) -> Vec<Link> {
        self.all().into_iter().copied().collect()
    }

    fn search(&mut self, source: u32, target: u32) -> Option<u32> {
        LinkStorage::search(self, source, target)
    }

    fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
        LinkStorage::get_or_create(self, source, target)
    }

    fn get_name(&mut self, id: u32) -> Result<Option<String>> {
        Ok(LinkStorage::get_name(self, id).cloned())
    }

    fn set_name(&mut self, id: u32, name: &str) -> Result<u32> {
        LinkStorage::set_name(self, id, name);
        Ok(id)
    }

    fn get_by_name(&mut self, name: &str) -> Result<Option<u32>> {
        Ok(LinkStorage::get_by_name(self, name))
    }

    fn remove_name(&mut self, id: u32) -> Result<()> {
        LinkStorage::remove_name(self, id);
        Ok(())
    }

    fn save(&mut self) -> Result<()> {
        LinkStorage::save(self)
    }

    fn get_or_create_named(&mut self, name: &str) -> Result<u32> {
        Ok(LinkStorage::get_or_create_named(self, name))
    }
}

impl NamedTypeLinks for NamedTypesDecorator {
    fn create(&mut self, source: u32, target: u32) -> u32 {
        NamedTypesDecorator::create(self, source, target)
    }

    fn ensure_created(&mut self, id: u32) -> u32 {
        NamedTypesDecorator::ensure_created(self, id)
    }

    fn get_link(&mut self, id: u32) -> Option<Link> {
        self.get(id).copied()
    }

    fn exists(&mut self, id: u32) -> bool {
        NamedTypesDecorator::exists(self, id)
    }

    fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
        NamedTypesDecorator::update(self, id, source, target)
    }

    fn delete(&mut self, id: u32) -> Result<Link> {
        NamedTypesDecorator::delete(self, id)
    }

    fn all_links(&mut self) -> Vec<Link> {
        self.all().into_iter().copied().collect()
    }

    fn search(&mut self, source: u32, target: u32) -> Option<u32> {
        NamedTypesDecorator::search(self, source, target)
    }

    fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
        NamedTypesDecorator::get_or_create(self, source, target)
    }

    fn get_name(&mut self, id: u32) -> Result<Option<String>> {
        NamedTypes::get_name(self, id)
    }

    fn set_name(&mut self, id: u32, name: &str) -> Result<u32> {
        NamedTypes::set_name(self, id, name)
    }

    fn get_by_name(&mut self, name: &str) -> Result<Option<u32>> {
        NamedTypes::get_by_name(self, name)
    }

    fn remove_name(&mut self, id: u32) -> Result<()> {
        NamedTypes::remove_name(self, id)
    }

    fn save(&mut self) -> Result<()> {
        NamedTypesDecorator::save(self)
    }
}

pub(crate) fn escape_lino_reference(reference: &str) -> String {
    if reference.is_empty() || reference.trim().is_empty() {
        return String::new();
    }

    let has_single_quote = reference.contains('\'');
    let has_double_quote = reference.contains('"');
    let needs_quoting = reference.contains(':')
        || reference.contains('(')
        || reference.contains(')')
        || reference.contains(' ')
        || reference.contains('\t')
        || reference.contains('\n')
        || reference.contains('\r')
        || has_single_quote
        || has_double_quote;

    if has_single_quote && has_double_quote {
        return format!("'{}'", reference.replace('\'', "\\'"));
    }

    if has_double_quote {
        return format!("'{reference}'");
    }

    if has_single_quote {
        return format!("\"{reference}\"");
    }

    if needs_quoting {
        return format!("'{reference}'");
    }

    reference.to_string()
}