bare-script 0.1.1

The type-safe scripting authority for Rust. A framework for building robust shell commands and automation with 'Parse, don't validate' philosophy.
Documentation
//! Type-safe argument building for commands.
//!
//! This module provides a type-safe way to build command arguments,
//! following the "Parse, don't validate" philosophy.

use std::ffi::{OsStr, OsString};

/// Represents the type of an argument.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgType {
    /// A flag (e.g., `-v`, `--verbose`)
    Flag,
    /// An option with a value (e.g., `-o file`, `--output=file`)
    Option,
    /// A positional argument
    Positional,
}

/// Represents a type-safe argument specification.
#[derive(Debug, Clone)]
pub struct Arg {
    /// The argument type
    pub arg_type: ArgType,
    /// Short form (e.g., `-v`)
    pub short: Option<&'static str>,
    /// Long form (e.g., `--verbose`)
    pub long: Option<&'static str>,
    /// Whether this argument is required
    pub required: bool,
    /// The argument value (if any)
    pub value: Option<OsString>,
}

impl Arg {
    /// Creates a new required flag argument.
    pub fn flag(short: &'static str, long: &'static str) -> Self {
        Self {
            arg_type: ArgType::Flag,
            short: Some(short),
            long: Some(long),
            required: true,
            value: None,
        }
    }

    /// Creates a new optional flag argument.
    pub fn optional_flag(short: &'static str, long: &'static str) -> Self {
        Self {
            arg_type: ArgType::Flag,
            short: Some(short),
            long: Some(long),
            required: false,
            value: None,
        }
    }

    /// Creates a new required option argument.
    pub fn option(short: &'static str, long: &'static str) -> Self {
        Self {
            arg_type: ArgType::Option,
            short: Some(short),
            long: Some(long),
            required: true,
            value: None,
        }
    }

    /// Creates a new optional option argument.
    pub fn optional_option(short: &'static str, long: &'static str) -> Self {
        Self {
            arg_type: ArgType::Option,
            short: Some(short),
            long: Some(long),
            required: false,
            value: None,
        }
    }

    /// Creates a new positional argument.
    pub fn positional() -> Self {
        Self {
            arg_type: ArgType::Positional,
            short: None,
            long: None,
            required: true,
            value: None,
        }
    }

    /// Creates a new optional positional argument.
    pub fn optional_positional() -> Self {
        Self {
            arg_type: ArgType::Positional,
            short: None,
            long: None,
            required: false,
            value: None,
        }
    }

    /// Sets the value for this argument.
    #[must_use]
    pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Self {
        self.value = Some(value.as_ref().to_os_string());
        self
    }

    /// Converts this argument to command-line strings.
    pub fn to_strings(&self) -> Vec<OsString> {
        let mut result = Vec::new();

        match self.arg_type {
            ArgType::Flag => {
                if let Some(short) = self.short {
                    result.push(OsString::from(short));
                }
            }
            ArgType::Option => {
                if let Some(short) = self.short {
                    if let Some(ref value) = self.value {
                        result.push(OsString::from(short));
                        result.push(value.clone());
                    }
                }
                if let Some(long) = self.long {
                    if let Some(ref value) = self.value {
                        let mut long_arg = OsString::from(long);
                        long_arg.push("=");
                        long_arg.push(value);
                        result.push(long_arg);
                    }
                }
            }
            ArgType::Positional => {
                if let Some(ref value) = self.value {
                    result.push(value.clone());
                }
            }
        }

        result
    }
}

/// A builder for type-safe command arguments.
#[derive(Debug, Default)]
pub struct Args {
    args: Vec<Arg>,
    positional: Vec<Arg>,
}

impl Args {
    /// Creates a new empty args builder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds a flag argument.
    pub fn flag(mut self, short: &'static str, long: &'static str) -> Self {
        self.args.push(Arg::flag(short, long));
        self
    }

    /// Adds an optional flag argument.
    pub fn optional_flag(mut self, short: &'static str, long: &'static str) -> Self {
        self.args.push(Arg::optional_flag(short, long));
        self
    }

    /// Adds an option argument (requires value).
    pub fn option(mut self, short: &'static str, long: &'static str) -> Self {
        self.args.push(Arg::option(short, long));
        self
    }

    /// Adds an optional option argument.
    pub fn optional_option(mut self, short: &'static str, long: &'static str) -> Self {
        self.args.push(Arg::optional_option(short, long));
        self
    }

    /// Adds a positional argument.
    pub fn positional(mut self) -> Self {
        self.positional.push(Arg::positional());
        self
    }

    /// Adds an optional positional argument.
    pub fn optional_positional(mut self) -> Self {
        self.positional.push(Arg::optional_positional());
        self
    }

    /// Sets the value for the last added argument that requires a value.
    ///
    /// Sets the value for the last added argument that requires a value.
    ///
    /// # Errors
    ///
    /// Returns an error if no argument expects a value.
    pub fn value<V: AsRef<OsStr>>(mut self, value: V) -> Result<Self, String> {
        // Find the last argument that can accept a value
        for arg in self.args.iter_mut().rev() {
            if arg.arg_type == ArgType::Option && arg.value.is_none() {
                arg.value = Some(value.as_ref().to_os_string());
                return Ok(self);
            }
        }

        // Check positional arguments
        for arg in self.positional.iter_mut().rev() {
            if arg.value.is_none() {
                arg.value = Some(value.as_ref().to_os_string());
                return Ok(self);
            }
        }

        Err("No argument expects a value".to_string())
    }

    /// Builds the argument list as a vector of OsString.
    ///
    /// Note: This consumes self. Use `to_vec()` for non-consuming access.
    pub fn build(self) -> Vec<OsString> {
        let mut result = Vec::new();

        // Add all args
        for arg in &self.args {
            result.extend(arg.to_strings());
        }

        // Add positional arguments
        for arg in &self.positional {
            result.extend(arg.to_strings());
        }

        result
    }

    /// Validates that all required arguments are provided.
    ///
    /// # Errors
    ///
    /// Returns an error if any required argument is missing.
    pub fn validate(&self) -> Result<(), String> {
        for arg in &self.args {
            // Flags don't require values - they're satisfied if present
            if arg.required && arg.value.is_none() && arg.arg_type != ArgType::Flag {
                let arg_name = arg.long.unwrap_or(arg.short.unwrap_or("unknown"));
                return Err(format!("Missing required argument: {arg_name}"));
            }
        }

        for arg in &self.positional {
            if arg.required && arg.value.is_none() {
                return Err("Missing required positional argument".to_string());
            }
        }

        Ok(())
    }
}

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

    #[test]
    fn test_flag() {
        let args = Args::new().flag("-v", "--verbose").build();

        assert!(args.contains(&OsString::from("-v")));
    }

    #[test]
    fn test_option() {
        let args = Args::new()
            .option("-o", "--output")
            .value("file.txt")
            .unwrap()
            .build();

        assert!(args.contains(&OsString::from("-o")));
        assert!(args.contains(&OsString::from("file.txt")));
    }

    #[test]
    fn test_positional() {
        let args = Args::new().positional().value("file.txt").unwrap().build();

        assert!(args.contains(&OsString::from("file.txt")));
    }

    #[test]
    fn test_validate_success() {
        let args = Args::new()
            .flag("-v", "--verbose")
            .option("-o", "--output")
            .value("file.txt")
            .unwrap();

        assert!(args.validate().is_ok());
    }

    #[test]
    fn test_validate_failure() {
        let args = Args::new()
            .option("-o", "--output")
            .value("file.txt")
            .unwrap()
            .positional();

        assert!(args.validate().is_err());
    }

    #[test]
    fn test_value_no_target() {
        let result = Args::new().value("test");
        assert!(result.is_err());
    }
}