dynamic-cli 0.10.0

A framework for building configurable CLI and REPL applications from YAML/JSON configuration files
Documentation
//! User interface module
//!
//! This module provides two main interfaces for interacting with the CLI framework:
//!
//! - [`CliInterface`]: One-shot command execution from command-line arguments
//! - [`ReplInterface`]: Interactive REPL (Read-Eval-Print Loop) with history
//!
//! # Overview
//!
//! The `interface` module is the user-facing layer of the framework. It handles:
//! - Parsing user input (CLI args or REPL lines)
//! - Executing commands through the registry
//! - Displaying results and errors
//! - Managing command history (REPL only)
//!
//! # Choosing an Interface
//!
//! ## CLI Interface
//!
//! Use [`CliInterface`] when:
//! - Running single commands from scripts
//! - Building traditional CLI tools
//! - No interaction is needed
//! - Each invocation is independent
//!
//! ```no_run
//! use dynamic_cli::interface::CliInterface;
//! use dynamic_cli::prelude::*;
//!
//! # #[derive(Default)]
//! # struct MyContext;
//! # impl ExecutionContext for MyContext {
//! #     fn as_any(&self) -> &dyn std::any::Any { self }
//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
//! # }
//! # fn main() -> dynamic_cli::Result<()> {
//! let registry = CommandRegistry::new();
//! let context = Box::new(MyContext::default());
//!
//! let cli = CliInterface::new(registry, context);
//! cli.run(std::env::args().skip(1).collect())?;
//! # Ok(())
//! # }
//! ```
//!
//! ## REPL Interface
//!
//! Use [`ReplInterface`] when:
//! - Building interactive tools
//! - Users need to run multiple commands
//! - Context/state is preserved between commands
//! - Command history and line editing are desired
//!
//! ```no_run
//! use dynamic_cli::interface::ReplInterface;
//! use dynamic_cli::prelude::*;
//!
//! # #[derive(Default)]
//! # struct MyContext;
//! # impl ExecutionContext for MyContext {
//! #     fn as_any(&self) -> &dyn std::any::Any { self }
//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
//! # }
//! # fn main() -> dynamic_cli::Result<()> {
//! let registry = CommandRegistry::new();
//! let context = Box::new(MyContext::default());
//!
//! let repl = ReplInterface::new(registry, context, "myapp".to_string(), None, None)?;
//! repl.run()?; // Enters interactive loop
//! # Ok(())
//! # }
//! ```
//!
//! # Architecture
//!
//! Both interfaces follow the same flow:
//!
//! ```text
//! User Input → Parser → Validator → Executor → Handler
//!                                        ↓
//!                                  ExecutionContext
//! ```
//!
//! **Key differences**:
//!
//! | Aspect | CLI | REPL |
//! |--------|-----|------|
//! | Input | Command-line args | Interactive lines |
//! | Parser | [`CliParser`] | [`ReplParser`] |
//! | History | None | Written to disk by `:quit` and Ctrl-D |
//! | Errors | Exit process | Display and continue |
//! | Lifecycle | One command, exit | Loop until `:quit`, `:exit` or Ctrl-D |
//!
//! # Error Handling
//!
//! ## CLI Interface
//!
//! Errors cause the process to exit with specific codes:
//! - `0`: Success
//! - `1`: Execution error
//! - `2`: Parse/validation error
//! - `3`: Other errors
//!
//! ## REPL Interface
//!
//! Errors are displayed but the REPL continues:
//! - Parse errors → show suggestions, continue
//! - Validation errors → explain issue, continue
//! - Execution errors → display error, continue
//! - Directive errors → list the available directives (unknown directive)
//!   or show the expected usage (wrong arguments), continue
//! - Input failure (the terminal cannot be read) → reported, the REPL exits
//!   without writing the history
//!
//! # REPL Directives
//!
//! In the REPL, a line starting with `:` is a framework directive; any
//! other line is an application command. The two never compete for a name:
//! `:quit` is always the directive, `quit` is always looked up among the
//! application's commands.
//!
//! | Directive          | Aliases    | Action                                      |
//! |--------------------|------------|---------------------------------------------|
//! | `:help [command]`  | `:h`, `:?` | Application help, or the help of `command`  |
//! | `:load <path>`     |            | Run every line of a script file             |
//! | `:quit`            | `:q`       | Write the session history, then leave       |
//! | `:exit`            |            | Leave without writing the session history   |
//!
//! These are the default names. An application can rename a directive,
//! replace its aliases or its description through the `directives:` section
//! of its configuration, for instance to translate them; it can neither add
//! nor disable one. See [`crate::config::directive`].
//!
//! Leaving the REPL:
//!
//! - `:quit` and end of input (Ctrl-D) write the session history, then
//!   leave.
//! - `:exit` leaves without writing it. Any other termination (a handler
//!   calling `std::process::exit`, a panic, the process being killed) also
//!   leaves the history file as it was when the session started.
//! - `exit` or `quit` typed without `:` is an ordinary command name. When
//!   the application has no command of that name, the error suggests the
//!   matching directive (`:quit`).
//!
//! Inside a `:load` script, `:quit` and `:exit` are reported and skipped;
//! the other lines run.
//!
//! `:help` lists the directives after the application help, under their
//! effective names (see
//! [`HelpFormatter::format_directives`](crate::help::HelpFormatter::format_directives)).
//! Tab completion after a leading `:` offers the directive names and
//! aliases. `--help` and `-h` work in the REPL as in CLI mode.
//!
//! # Examples
//!
//! ## Complete CLI Application
//!
//! ```no_run
//! use dynamic_cli::prelude::*;
//! use dynamic_cli::config::loader::load_config;
//! use dynamic_cli::interface::CliInterface;
//!
//! // Define context
//! #[derive(Default)]
//! struct AppContext {
//!     data: Vec<String>,
//! }
//!
//! impl ExecutionContext for AppContext {
//!     fn as_any(&self) -> &dyn std::any::Any { self }
//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
//! }
//!
//! // Define handler
//! struct AddCommand;
//!
//! impl CommandHandler for AddCommand {
//!     fn execute(
//!         &self,
//!         context: &mut dyn ExecutionContext,
//!         args: &ParsedArgs,
//!     ) -> dynamic_cli::Result<()> {
//!         let ctx = dynamic_cli::context::downcast_mut::<AppContext>(context).unwrap();
//!         let item = args.get_scalar("item").unwrap();
//!         ctx.data.push(item.to_string());
//!         println!("Added: {}", item);
//!         Ok(())
//!     }
//! }
//!
//! fn main() -> dynamic_cli::Result<()> {
//!     // Load configuration
//!     let config = load_config("commands.yaml")?;
//!     
//!     // Build registry
//!     let mut registry = CommandRegistry::new();
//!     registry.register_sync(
//!         config.commands[0].clone(),
//!         Box::new(AddCommand),
//!     )?;
//!     
//!     // Create and run CLI
//!     let context = Box::new(AppContext::default());
//!     let cli = CliInterface::new(registry, context);
//!     cli.run(std::env::args().skip(1).collect())
//! }
//! ```
//!
//! ## Complete REPL Application
//!
//! ```no_run
//! use dynamic_cli::prelude::*;
//! use dynamic_cli::config::loader::load_config;
//! use dynamic_cli::interface::ReplInterface;
//!
//! // Same context and handler as above
//! # #[derive(Default)]
//! # struct AppContext { data: Vec<String> }
//! # impl ExecutionContext for AppContext {
//! #     fn as_any(&self) -> &dyn std::any::Any { self }
//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
//! # }
//! # struct AddCommand;
//! # impl CommandHandler for AddCommand {
//! #     fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> dynamic_cli::Result<()> {
//! #         let ctx = dynamic_cli::context::downcast_mut::<AppContext>(context).unwrap();
//! #         let item = args.get_scalar("item").unwrap();
//! #         ctx.data.push(item.to_string());
//! #         println!("Added: {}", item);
//! #         Ok(())
//! #     }
//! # }
//!
//! fn main() -> dynamic_cli::Result<()> {
//!     // Load configuration
//!     let config = load_config("commands.yaml")?;
//!     
//!     // Build registry
//!     let mut registry = CommandRegistry::new();
//!     registry.register_sync(
//!         config.commands[0].clone(),
//!         Box::new(AddCommand),
//!     )?;
//!     
//!     // Create and run REPL
//!     let context = Box::new(AppContext::default());
//!     let repl = ReplInterface::new(registry, context, "myapp".to_string(), None, None)?;
//!     repl.run() // Interactive loop
//! }
//! ```
//!
//! # Module Structure
//!
//! - [`cli`]: CLI interface implementation
//! - [`repl`]: REPL interface implementation
//!
//! [`CliParser`]: crate::parser::CliParser
//! [`ReplParser`]: crate::parser::ReplParser

pub mod cli;
pub mod repl;

// Re-export main types for convenience
pub use cli::{CliInterface, ScriptErrorPolicy, ScriptOutcome};
pub use repl::ReplInterface;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::schema::CommandDefinition;
    use crate::prelude::*;

    // Test context
    struct TestContext;

    impl ExecutionContext for TestContext {
        fn as_any(&self) -> &dyn std::any::Any {
            self
        }

        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
            self
        }
    }

    // Test handler
    struct TestHandler;

    impl CommandHandler for TestHandler {
        fn execute(
            &self,
            _context: &mut dyn ExecutionContext,
            _args: &ParsedArgs,
        ) -> crate::Result<()> {
            Ok(())
        }
    }

    #[test]
    fn test_module_imports() {
        // Verify that types are re-exported
        let _: Option<CliInterface> = None;
        let _: Option<ReplInterface> = None;
    }

    #[test]
    fn test_cli_interface_accessible() {
        let mut registry = CommandRegistry::new();

        let cmd_def = CommandDefinition {
            name: "test".to_string(),
            aliases: vec![],
            description: "Test".to_string(),
            required: false,
            arguments: vec![],
            options: vec![],
            implementation: "test".to_string(),
            continue_on_failure: false,
            requires_success: false,
        };

        registry
            .register_sync(cmd_def, Box::new(TestHandler))
            .unwrap();

        let context = Box::new(TestContext);
        let _cli = CliInterface::new(registry, context);
    }

    #[test]
    fn test_repl_interface_accessible() {
        let mut registry = CommandRegistry::new();

        let cmd_def = CommandDefinition {
            name: "test".to_string(),
            aliases: vec![],
            description: "Test".to_string(),
            required: false,
            arguments: vec![],
            options: vec![],
            implementation: "test".to_string(),
            continue_on_failure: false,
            requires_success: false,
        };

        registry
            .register_sync(cmd_def, Box::new(TestHandler))
            .unwrap();

        let context = Box::new(TestContext);
        let _repl = ReplInterface::new(registry, context, "test".to_string(), None, None);
    }
}