|
| 1 | +# `rudof_cli` |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The `rudof_cli` crate provides the command-line interface for the Rudof toolkit, that provides a comprehensive suite of tools for working with RDF data and schema languages. It enables users to inspect and validate RDF data using ShEx or SHACL, as well as convert between different RDF modeling languages such as ShEx, SHACL, and DCTAP. |
| 6 | + |
| 7 | +## Architecture and Package Structure |
| 8 | + |
| 9 | +The `rudof_cli` crate is organized around a **command pattern** with the following layers: |
| 10 | + |
| 11 | +### 1. CLI Layer (`cli/`) |
| 12 | + |
| 13 | +Handles command-line argument parsing using Clap, defining the user interface and available commands. |
| 14 | + |
| 15 | +- **`parser.rs`**: Defines the complete CLI structure using Clap's derive macros, including all commands, subcommands, and their arguments. This module uses a declarative approach where commands are represented as enum variants, ensuring type safety and exhaustive pattern matching. Common arguments (configuration paths, output destinations, force overwrite flags) are abstracted into reusable structures like `CommonArgs`, `CommonArgsAll`, and `CommonArgsOutputForceOverWrite` to eliminate duplication. The parser exclusively uses CLI wrapper types (e.g., `DataFormatCli`) instead of library types directly, creating a clean decoupling between the presentation layer and core business logic. |
| 16 | + |
| 17 | +- **`wrappers.rs`**: Provides CLI-friendly wrappers for core library types using a macro-based approach implemented through the `cli_wrapper!` macro. This macro automatically generates all necessary boilerplate code for creating enums with Clap's `ValueEnum` support, bidirectional conversion between CLI and library types, display formatting for help messages, and optional `MimeType` trait implementations. |
| 18 | + |
| 19 | +### 2. Command Layer (`commands/`) |
| 20 | + |
| 21 | +Implements each command using a unified trait-based approach defined in `commands/base.rs`. |
| 22 | + |
| 23 | +- **`base.rs`**: The foundation of the command system, containing three key components: |
| 24 | + |
| 25 | + - **`Command` trait**: The core interface that all commands must implement, defining an `execute(&self, ctx: &mut CommandContext)` method for command logic and a `name(&self)` method for identification. This enables polymorphic command execution where the main application works with trait objects without knowing concrete command types. |
| 26 | + |
| 27 | + - **`CommandContext`**: The shared execution environment that acts as a dependency injection container. It provides commands with a `Box<dyn Write>` output writer (supporting stdout, files, or other destinations), a configured `Rudof` instance initialized with all settings, a debug level for verbosity control, and color support detection. The `from_cli()` factory method bridges CLI parsing and command execution by loading configuration files, initializing the Rudof library, creating appropriate output writers, and detecting terminal capabilities—shielding commands from initialization complexity. |
| 28 | + |
| 29 | + - **`CommandFactory`**: A factory pattern implementation that centralizes command instantiation logic. The `create()` method maps CLI command enum variants to their corresponding `Command` trait objects through type erasure. This design follows the Open-Closed Principle: the system is open for extension (new commands can be added by adding a single match arm) but closed for modification (existing command handling remains unchanged). |
| 30 | + |
| 31 | +- **Individual command modules** (`shex.rs`, `shacl.rs`, `validate.rs`, `data.rs`, `node.rs`, etc.): Each implements the `Command` trait to provide specific functionality. Commands follow a consistent execution pattern: (1) convert CLI wrapper types to library types, (2) execute core logic by calling Rudof library methods and (3) write results to the context's output writer. |
| 32 | + |
| 33 | +### 3. Output Layer (`output/`) |
| 34 | + |
| 35 | +Manages output formatting with automatic color support detection and configurable writers. |
| 36 | + |
| 37 | +- **`color.rs`**: Detects terminal color capabilities through a three-state model (Always, Never, Auto). Respects explicit user preferences via `FORCE_COLOR` and `NO_COLOR` environment variables, automatically detects terminal capabilities using the `supports-color` crate, handles CI environment detection where colored output may not render correctly, and caches detection results to avoid repeated system calls for performance optimization. |
| 38 | + |
| 39 | +- **`writer.rs`**: Creates appropriate output writers based on command-line arguments. When no output file is specified, returns stdout with automatic color detection. When an output file is specified, creates a file handle with overwrite protection and disables color output (since files don't support ANSI codes). |
| 40 | + |
| 41 | +### Command Lifecycle |
| 42 | + |
| 43 | +The `main.rs` orchestrates the complete command lifecycle through five distinct phases: |
| 44 | + |
| 45 | +1. **Setup**: Initializes the application environment by loading `.env` files with environment variables, configuring the tracing subsystem for structured logging (writing to stderr with file and line number information), and setting necessary environment variables for dependent libraries. This establishes the foundational runtime environment. |
| 46 | + |
| 47 | +2. **Parsing**: Safely handles command-line arguments using Clap, including non-UTF8 paths that can occur on some operating systems. Through `clientele::args_os()` and `Cli::parse_from()`, raw arguments are transformed into the strongly-typed `Cli` structure with all validation applied, ensuring only well-formed commands proceed to execution. |
| 48 | + |
| 49 | +3. **Factory**: Uses `CommandFactory::create()` to instantiate the appropriate command implementation from the parsed CLI enum. The factory performs type erasure, returning a `Box<dyn Command>` trait object that enables polymorphic handling without the main function needing to know concrete command types. |
| 50 | + |
| 51 | +4. **Context**: Builds the execution environment through `CommandContext::from_cli()`, which loads the configuration file (if specified), initializes the Rudof library with loaded settings, creates the appropriate output writer (stdout or file) based on CLI flags, and detects terminal color capabilities. Commands receive a fully prepared context with all dependencies resolved. |
| 52 | + |
| 53 | +5. **Execution**: Invokes `command.execute(&mut ctx)` to run the command's business logic. Errors are automatically propagated up the call stack using the `?` operator and handled uniformly by the main function, where they're reported to the user with rich context information. This separation of phases makes the application's execution flow predictable and maintainable. |
| 54 | + |
| 55 | +## Adding a New Command |
| 56 | + |
| 57 | +This section provides a step-by-step guide to adding a new command to the CLI. |
| 58 | + |
| 59 | +### Step 1: Define Command Arguments in `cli/parser.rs` |
| 60 | + |
| 61 | +Add your command's arguments structure: |
| 62 | + |
| 63 | +```rust |
| 64 | +#[derive(Args, Debug, Clone)] |
| 65 | +pub struct MyCommandArgs { |
| 66 | + /// Common arguments (config, output, etc.) |
| 67 | + #[command(flatten)] |
| 68 | + pub common: CommonArgsAll, |
| 69 | + |
| 70 | + /// Input data file |
| 71 | + #[arg(short, long)] |
| 72 | + pub input: String, |
| 73 | + |
| 74 | + /// Optional parameter with default |
| 75 | + #[arg(short, long, default_value = "default_value")] |
| 76 | + pub option: String, |
| 77 | + |
| 78 | + /// Using a CLI wrapper type |
| 79 | + #[arg(short, long, value_enum)] |
| 80 | + pub format: DataFormatCli, |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +Add the command variant to the `Command` enum: |
| 85 | + |
| 86 | +```rust |
| 87 | +#[derive(Subcommand, Debug, Clone)] |
| 88 | +pub enum Command { |
| 89 | + // ... existing commands ... |
| 90 | + |
| 91 | + /// Brief description of your command |
| 92 | + MyCommand(MyCommandArgs), |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +### Step 2: Add Common Args Handling in `commands/base.rs` |
| 97 | + |
| 98 | +Update the `extract_common()` function to handle your command: |
| 99 | + |
| 100 | +```rust |
| 101 | +fn extract_common(cmd: &CliCommand) -> CommonArgs { |
| 102 | + match cmd { |
| 103 | + // ... existing matches ... |
| 104 | + |
| 105 | + CliCommand::MyCommand(a) => CommonArgs::All(CommonArgsAll { |
| 106 | + config: a.common.config.clone(), |
| 107 | + output: a.common.output.clone(), |
| 108 | + force_overwrite: a.common.force_overwrite, |
| 109 | + }), |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Step 3: Create Command Implementation File |
| 115 | + |
| 116 | +Create `commands/my_command.rs`: |
| 117 | + |
| 118 | +```rust |
| 119 | +use crate::cli::parser::MyCommandArgs; |
| 120 | +use crate::commands::base::{Command, CommandContext}; |
| 121 | +use anyhow::Result; |
| 122 | + |
| 123 | +/// Implementation of the `my-command` command. |
| 124 | +/// |
| 125 | +/// Detailed description of what this command does. |
| 126 | +pub struct MyCommand { |
| 127 | + /// Arguments specific to this command. |
| 128 | + args: MyCommandArgs, |
| 129 | +} |
| 130 | + |
| 131 | +impl MyCommand { |
| 132 | + pub fn new(args: MyCommandArgs) -> Self { |
| 133 | + Self { args } |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +impl Command for MyCommand { |
| 138 | + fn name(&self) -> &'static str { |
| 139 | + "my-command" |
| 140 | + } |
| 141 | + |
| 142 | + fn execute(&self, ctx: &mut CommandContext) -> Result<()> { |
| 143 | + // 1. Convert CLI types to library types |
| 144 | + let format = (&self.args.format).into(); |
| 145 | + |
| 146 | + // 2. Execute your command logic |
| 147 | + // ... your implementation ... |
| 148 | + |
| 149 | + // 3. Write output |
| 150 | + writeln!(ctx.writer, "Command executed successfully")?; |
| 151 | + |
| 152 | + Ok(()) |
| 153 | + } |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +### Step 4: Register Command in Module System |
| 158 | + |
| 159 | +Update `commands/mod.rs`: |
| 160 | + |
| 161 | +```rust |
| 162 | +mod my_command; // Add module declaration |
| 163 | + |
| 164 | +pub use my_command::MyCommand; // Export the command |
| 165 | +``` |
| 166 | + |
| 167 | +### Step 5: Add to Command Factory |
| 168 | + |
| 169 | +Update the factory in `commands/base.rs`: |
| 170 | + |
| 171 | +```rust |
| 172 | +impl CommandFactory { |
| 173 | + pub fn create(cli_command: CliCommand) -> Result<Box<dyn Command>> { |
| 174 | + match cli_command { |
| 175 | + // ... existing matches ... |
| 176 | + |
| 177 | + CliCommand::MyCommand(args) => Ok(Box::new(MyCommand::new(args))), |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +### Step 6: Update Documentation |
| 184 | + |
| 185 | +Add usage documentation in appropriate places. |
| 186 | + |
| 187 | +## Dependencies |
| 188 | + |
| 189 | +### Main Dependencies |
| 190 | + |
| 191 | +The CLI relies on the following external crates: |
| 192 | + |
| 193 | +- **`clap`**: Command-line argument parsing with derive macros |
| 194 | +- **`clap_complete_command`**: Shell completion generation |
| 195 | +- **`tokio`**: Async runtime for concurrent operations |
| 196 | +- **`anyhow`** / **`thiserror`**: Error handling |
| 197 | +- **`tracing`** / **`tracing-subscriber`**: Structured logging |
| 198 | +- **`tabled`**: Table formatting for output |
| 199 | +- **`supports-color`**: Terminal color capability detection |
| 200 | +- **`clientele`**: CLI utility helpers |
0 commit comments