incurs 0.1.1

A declarative CLI framework for Rust with built-in JSON/CSV output, MCP support, and shell completions
Documentation
//! Schema trait and field metadata for the incur framework.
//!
//! This module defines the `IncurSchema` trait that derive macros generate,
//! providing field metadata and raw-value parsing that the parser, help system,
//! completions, and skill generation all depend on.

use serde_json::Value;
use std::collections::BTreeMap;

use crate::errors::ValidationError;

/// The type of a field in a schema.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldType {
    String,
    Number,
    Boolean,
    Array(Box<FieldType>),
    Enum(Vec<String>),
    Count,
    Value,
}

impl FieldType {
    /// Returns a human-readable type name for help output.
    pub fn display_name(&self) -> String {
        match self {
            FieldType::String => "string".to_string(),
            FieldType::Number => "number".to_string(),
            FieldType::Boolean => "boolean".to_string(),
            FieldType::Array(_) => "array".to_string(),
            FieldType::Enum(values) => values.join("|"),
            FieldType::Count => "count".to_string(),
            FieldType::Value => "value".to_string(),
        }
    }
}

/// Metadata about a single field in a schema.
#[derive(Debug, Clone)]
pub struct FieldMeta {
    /// The field name (Rust identifier, in snake_case).
    pub name: &'static str,
    /// The field's CLI name (kebab-case version of name).
    pub cli_name: String,
    /// Human-readable description (from doc comments).
    pub description: Option<&'static str>,
    /// The field's type.
    pub field_type: FieldType,
    /// Whether the field is required.
    pub required: bool,
    /// Default value, if any.
    pub default: Option<Value>,
    /// Short alias (single char).
    pub alias: Option<char>,
    /// Whether the field is deprecated.
    pub deprecated: bool,
    /// Environment variable name (for Env schemas).
    pub env_name: Option<&'static str>,
}

/// Trait for types that can describe themselves as a schema and parse from raw values.
///
/// Generated by `#[derive(incurs::Args)]`, `#[derive(incurs::Options)]`, and `#[derive(incurs::Env)]`.
pub trait IncurSchema: Sized {
    /// Returns metadata for all fields in this schema.
    fn fields() -> Vec<FieldMeta>;

    /// Parses from a map of raw string/value pairs.
    fn from_raw(raw: &BTreeMap<String, Value>) -> std::result::Result<Self, ValidationError>;

    /// Returns the names of all fields (for option name lookups).
    fn field_names() -> Vec<&'static str> {
        Self::fields().iter().map(|f| f.name).collect()
    }
}

/// Converts a Rust snake_case name to CLI kebab-case.
pub fn to_kebab(name: &str) -> String {
    let mut result = String::with_capacity(name.len());
    for (i, ch) in name.chars().enumerate() {
        if ch == '_' {
            result.push('-');
        } else if ch.is_uppercase() {
            if i > 0 {
                result.push('-');
            }
            result.push(ch.to_lowercase().next().unwrap_or(ch));
        } else {
            result.push(ch);
        }
    }
    result
}

/// Converts a CLI kebab-case name back to Rust snake_case.
pub fn to_snake(name: &str) -> String {
    name.replace('-', "_")
}

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

    #[test]
    fn test_to_kebab() {
        assert_eq!(to_kebab("filter_output"), "filter-output");
        assert_eq!(to_kebab("tokenLimit"), "token-limit");
        assert_eq!(to_kebab("simple"), "simple");
    }

    #[test]
    fn test_to_snake() {
        assert_eq!(to_snake("filter-output"), "filter_output");
        assert_eq!(to_snake("simple"), "simple");
    }
}