xdiff-live 0.1.1

A live diff tool for comparing files and directories.
Documentation
//! Command-line interface definitions for XDiff-NG tools.
//!
//! This module provides the command-line argument parsing structures and utilities
//! for both `xdiff` and `xreq` tools using the `clap` crate. It defines the main
//! command structure, subcommands, and parameter parsing logic.

// clap是一个简单易用,功能强大的命令行参数解析库。
// clap允许多种方式指定我们的命令行。支持常规的Rust方法调用、宏或者YAML配置。
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};

use crate::ExtraArgs;

/// Main command-line arguments structure for XDiff-NG tools.
///
/// This structure defines the top-level CLI interface that accepts subcommands
/// for different operations like running comparisons or parsing URLs.
///
/// # Examples
///
/// ```
/// use clap::Parser;
/// use xdiff_live::cli::Args;
///
/// // Parse arguments from command line
/// let args = Args::parse();
/// ```
#[derive(Parser, Debug, Clone)]
#[clap(version, author, about, long_about = None)]
pub struct Args {
    /// The subcommand to execute
    #[clap(subcommand)]
    pub action: Action,
    // 属性可以与实现子命令的结构字段或枚举变体一起使用
    // 使用 #[clap(subcommand)] 属性宏可以将一个结构体或枚举类型标记为一个 CLI 的子命令。
}

/// Available subcommands for the CLI application.
///
/// This enum defines all the possible actions that can be performed by the
/// XDiff-NG tools. Currently supports running comparisons and parsing URLs.
#[derive(Subcommand, Debug, Clone)]
#[non_exhaustive]
// non_exhaustive属性表示类型或变体将来可能会添加更多字段或变体。
// 它可以应用在结构体(struct)上、枚举(enum)上和枚举变体上
pub enum Action {
    // #[clap(about = "Diff two http requests and compare the responses.")]
    /// Diff two API responses based on given profile
    Run(RunArgs),
    /// Parse URls to generate a profile
    Parse,
}

/// Arguments for the 'run' subcommand.
///
/// This structure contains all the parameters needed to execute a comparison
/// or request using predefined profiles, including profile selection,
/// configuration file path, and extra parameters for customization.
///
/// # Examples
///
/// ```bash
/// # Using the run command with various options
/// xdiff run -p myprofile -c config.yml -e param1=value1 -e @header1=value1
/// ```
#[derive(Parser, Debug, Clone)]
pub struct RunArgs {
    /// Profile Name
    #[clap(short, long, value_parser)]
    pub profile: String,

    /// Overrides Args. Could be used to override the query, headers, and body of the request
    /// for query params, use `-e key=value`
    /// for headers, use `-e %key=value`
    /// for body, use `-e @key=value`
    #[clap(short, long, value_parser = parse_key_val, number_of_values = 1)]
    pub extra_params: Vec<KeyVal>,
    
    /// Configuration to use
    #[clap(short, long, value_parser)]
    pub config: Option<String>,
}

/// Types of key-value parameters that can be specified via command line.
///
/// This enum categorizes the different types of parameters that can be
/// overridden or added through the `-e` command-line option.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyValType {
    /// Query parameters (URL parameters)
    Query,
    /// HTTP headers
    Header,
    /// Request body parameters
    Body,
}

/// A key-value pair with a specific type designation.
///
/// This structure represents a single parameter override that can be
/// applied to HTTP requests. The type determines where the parameter
/// will be applied (query, header, or body).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyVal {
    /// The type of parameter (query, header, or body)
    key_type: KeyValType,
    /// The parameter name/key
    key: String,
    /// The parameter value
    value: String,
}

/// Parses a key-value string into a `KeyVal` structure.
///
/// This function takes a string in the format `[prefix]key=value` and parses it
/// into a structured `KeyVal` object. The prefix determines the parameter type:
///
/// - No prefix or alphabetic start: Query parameter
/// - `%` prefix: Header parameter  
/// - `@` prefix: Body parameter
///
/// # Arguments
///
/// * `s` - The input string to parse (e.g., "key=value", "%header=value", "@body=value")
///
/// # Returns
///
/// A `Result<KeyVal>` containing the parsed key-value pair, or an error if
/// the input format is invalid.
///
/// # Examples
///
/// ```
/// use xdiff_live::cli::{parse_key_val, KeyValType};
///
/// // Parse a query parameter
/// let kv = parse_key_val("search=rust").unwrap();
/// assert_eq!(kv.key_type, KeyValType::Query);
///
/// // Parse a header parameter
/// let kv = parse_key_val("%Authorization=Bearer token").unwrap();
/// assert_eq!(kv.key_type, KeyValType::Header);
///
/// // Parse a body parameter
/// let kv = parse_key_val("@username=john").unwrap();
/// assert_eq!(kv.key_type, KeyValType::Body);
/// ```
///
/// # Errors
///
/// Returns an error if:
/// - The input doesn't contain an `=` sign
/// - The key starts with an invalid character
/// - Either key or value is missing
pub fn parse_key_val(s: &str) -> Result<KeyVal> {
    let mut parts = s.splitn(2, "=");

    let key = parts
        .next()
        .ok_or_else(|| anyhow!("Invalid key value pair: {}", s))?
        .trim();
    let value = parts
        .next()
        .ok_or_else(|| anyhow!("Invalid key value pair: {}", s))?
        .trim();

    let (key_type, key) = match key.chars().next() {
        Some('%') => (KeyValType::Header, &key[1..]),
        Some('@') => (KeyValType::Body, &key[1..]),
        Some(v) if v.is_ascii_alphabetic() => (KeyValType::Query, key),
        _ => return Err(anyhow!("Invalid key value pair")),
    };

    Ok(KeyVal {
        key_type,
        key: key.to_string(),
        value: value.to_string(),
    })
}

impl From<Vec<KeyVal>> for ExtraArgs {
    /// Converts a vector of `KeyVal` items into an `ExtraArgs` structure.
    ///
    /// This implementation groups the key-value pairs by their type and creates
    /// separate vectors for headers, query parameters, and body parameters.
    ///
    /// # Arguments
    ///
    /// * `args` - A vector of `KeyVal` items to be converted
    ///
    /// # Returns
    ///
    /// An `ExtraArgs` structure with the key-value pairs organized by type.
    ///
    /// # Examples
    ///
    /// ```
    /// use xdiff_live::cli::{KeyVal, KeyValType};
    /// use xdiff_live::ExtraArgs;
    ///
    /// let kv_pairs = vec![
    ///     KeyVal { /* query param */ },
    ///     KeyVal { /* header param */ },
    ///     KeyVal { /* body param */ },
    /// ];
    ///
    /// let extra_args: ExtraArgs = kv_pairs.into();
    /// ```
    fn from(args: Vec<KeyVal>) -> Self {
        let mut headers = vec![];
        let mut query = vec![];
        let mut body = vec![];

        for arg in args {
            match arg.key_type {
                KeyValType::Header => headers.push((arg.key, arg.value)),
                KeyValType::Query => query.push((arg.key, arg.value)),
                KeyValType::Body => body.push((arg.key, arg.value)),
            }
        }

        Self {
            headers,
            query,
            body,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::vec;

    use super::*;

    /// Tests the conversion from `Vec<KeyVal>` to `ExtraArgs`.
    ///
    /// This test verifies that key-value pairs are correctly grouped by type
    /// when converting to the `ExtraArgs` structure.
    #[test]
    fn from_vec_key_val_for_extra_args_should_work() {
        let args = vec![
            KeyVal {
                key_type: KeyValType::Header,
                key: "key1".to_string(),
                value: "value1".to_string(),
            },
            KeyVal {
                key_type: KeyValType::Query,
                key: "key2".to_string(),
                value: "value2".to_string(),
            },
            KeyVal {
                key_type: KeyValType::Body,
                key: "key3".to_string(),
                value: "value3".to_string(),
            },
        ];

        let extra_args = ExtraArgs::from(args);

        assert_eq!(
            extra_args,
            ExtraArgs {
                headers: vec![("key1".to_string(), "value1".to_string())],
                query: vec![("key2".to_string(), "value2".to_string())],
                body: vec![("key3".to_string(), "value3".to_string())],
            }
        );
    }

    /// Tests the `parse_key_val` function with various input formats.
    ///
    /// This test verifies that the parser correctly identifies and categorizes
    /// different types of key-value pairs based on their prefix characters.
    #[test]
    fn parse_key_val_should_work() {
        let args = vec![
            "%key1=value1",
            "key2=value2",
            "@key3=value3",
            "key4=value4",
        ];

        let key_vals = args
            .into_iter()
            .map(|arg| parse_key_val(arg))
            .collect::<Result<Vec<_>>>()
            .unwrap();

        assert_eq!(
            key_vals,
            vec![
                KeyVal {
                    key_type: KeyValType::Header,
                    key: "key1".to_string(),
                    value: "value1".to_string(),
                },
                KeyVal {
                    key_type: KeyValType::Query,
                    key: "key2".to_string(),
                    value: "value2".to_string(),
                },
                KeyVal {
                    key_type: KeyValType::Body,
                    key: "key3".to_string(),
                    value: "value3".to_string(),
                },
                KeyVal {
                    key_type: KeyValType::Query,
                    key: "key4".to_string(),
                    value: "value4".to_string(),
                },
            ]

        );
    }
}