d2o 0.1.1

Help to Options - Parse help or manpage texts and generate shell completion scripts
use ecow::{EcoString, EcoVec};
use foldhash::quality::RandomState;
use scc::{HashMap as SccHashMap, HashSet as SccHashSet};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;

pub type HashMap<K, V> = SccHashMap<K, V, RandomState>;
pub type HashSet<T> = SccHashSet<T, RandomState>;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Command {
    pub name: EcoString,
    pub description: EcoString,
    pub usage: EcoString,
    pub options: EcoVec<Opt>,
    #[serde(default)]
    pub subcommands: EcoVec<Command>,
    #[serde(default)]
    pub version: EcoString,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opt {
    pub names: EcoVec<OptName>,
    pub argument: EcoString,
    pub description: EcoString,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
pub struct OptName {
    pub raw: EcoString,
    #[serde(rename = "type")]
    pub opt_type: OptNameType,
}

impl<'de> Deserialize<'de> for OptName {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum OptNameCompat {
            Legacy(String),
            Structured {
                raw: EcoString,
                #[serde(rename = "type")]
                opt_type: OptNameType,
            },
        }

        match OptNameCompat::deserialize(deserializer)? {
            OptNameCompat::Legacy(s) => {
                let opt_type = OptName::determine_type(&s)
                    .ok_or_else(|| serde::de::Error::custom("invalid option name"))?;
                Ok(OptName {
                    raw: EcoString::from(s),
                    opt_type,
                })
            }
            OptNameCompat::Structured { raw, opt_type } => Ok(OptName { raw, opt_type }),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "UPPERCASE")]
pub enum OptNameType {
    LongType,
    ShortType,
    OldType,
    DoubleDashAlone,
    SingleDashAlone,
}

impl PartialOrd for OptName {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for OptName {
    fn cmp(&self, other: &Self) -> Ordering {
        (&self.raw, &self.opt_type).cmp(&(&other.raw, &other.opt_type))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Subcommand {
    pub cmd: EcoString,
    pub desc: EcoString,
}

impl OptName {
    pub fn new(raw: EcoString, opt_type: OptNameType) -> Self {
        Self { raw, opt_type }
    }

    pub fn from_text(s: &str) -> Option<Self> {
        let opt_type = Self::determine_type(s)?;
        Some(Self {
            raw: EcoString::from(s),
            opt_type,
        })
    }

    fn determine_type(s: &str) -> Option<OptNameType> {
        match s {
            "-" => Some(OptNameType::SingleDashAlone),
            "--" => Some(OptNameType::DoubleDashAlone),
            s if s.starts_with("--") => Some(OptNameType::LongType),
            s if s.starts_with('-') && s.len() == 2 => Some(OptNameType::ShortType),
            s if s.starts_with('-') => Some(OptNameType::OldType),
            _ => None,
        }
    }
}

impl std::fmt::Display for OptName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.raw)
    }
}

impl std::fmt::Display for Opt {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let names = self
            .names
            .iter()
            .map(|n| n.raw.to_string())
            .collect::<Vec<_>>()
            .join(" ");
        write!(
            f,
            "{}  ::  {}\n{}\n",
            names, self.argument, self.description
        )
    }
}

impl std::fmt::Display for Subcommand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:<25} ({})", self.cmd, self.desc)
    }
}

impl Command {
    pub fn new(name: EcoString) -> Self {
        Self {
            name,
            description: EcoString::new(),
            usage: EcoString::new(),
            options: EcoVec::new(),
            subcommands: EcoVec::new(),
            version: EcoString::new(),
        }
    }

    pub fn as_subcommand(&self) -> Subcommand {
        Subcommand {
            cmd: self.name.clone(),
            desc: self.description.clone(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_command_new_and_as_subcommand() {
        let mut cmd = Command::new(EcoString::from("test"));
        assert_eq!(cmd.name.as_str(), "test");
        assert!(cmd.description.is_empty());

        cmd.description = EcoString::from("Test command");
        let sub = cmd.as_subcommand();
        assert_eq!(sub.cmd.as_str(), "test");
        assert_eq!(sub.desc.as_str(), "Test command");
    }
}