From fd733a87364b877d5e66b4973bd61b5379ec4d61 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 16 Jun 2026 21:59:35 +0900 Subject: feat(command): implement Symfony Command --- .../src/symfony/console/command/command.rs | 1008 ++++++++++---------- 1 file changed, 527 insertions(+), 481 deletions(-) (limited to 'crates/shirabe-external-packages/src/symfony/console/command/command.rs') diff --git a/crates/shirabe-external-packages/src/symfony/console/command/command.rs b/crates/shirabe-external-packages/src/symfony/console/command/command.rs index d811184..2040f95 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs @@ -4,7 +4,6 @@ use crate::symfony::console::application::Application; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::console::exception::logic_exception::LogicException; use crate::symfony::console::helper::helper_set::HelperSet; use crate::symfony::console::input::input_argument::InputArgument; use crate::symfony::console::input::input_definition::InputDefinition; @@ -16,12 +15,14 @@ use shirabe_php_shim::PhpMixed; use std::cell::RefCell; use std::rc::Rc; -/// Base class for all commands. +/// The base-class state of the PHP `Command` class. /// -/// Phase B: the PHP `Command` class is split into the polymorphic `Command` trait -/// (defined below) and this concrete `BaseCommand` struct holding the base-class -/// state and behavior. Subclasses embed a `BaseCommand` and implement `Command`. -pub struct BaseCommand { +/// The PHP `Command` class is split into the polymorphic [`Command`] trait (the +/// methods callers invoke on a command of unknown concrete type) and this struct, +/// which holds the base-class fields and provides their canonical behavior via +/// `impl Command for CommandData`. Subclasses embed a `CommandData` (directly, or +/// transitively through `BaseCommandData`) and forward the state methods to it. +pub struct CommandData { application: Option>>, name: Option, process_title: Option, @@ -39,7 +40,7 @@ pub struct BaseCommand { helper_set: Option>>, } -impl BaseCommand { +impl CommandData { // see https://tldp.org/LDP/abs/html/exitcodes.html pub const SUCCESS: i64 = 0; pub const FAILURE: i64 = 1; @@ -66,16 +67,21 @@ impl BaseCommand { todo!() } - /// `$name` is the name of the command; passing None means it must be set in configure(). + /// Builds the base-class state. `name` is the name of the command; passing None + /// means it must be set in the subclass `configure()`. /// - /// Throws LogicException when the command name is empty. - pub fn __construct(name: Option) -> anyhow::Result { - let mut this = BaseCommand { + /// Unlike PHP's `__construct`, this does not call `configure()` — the concrete + /// command's `new()` calls `configure()` after embedding the data, mirroring the + /// virtual dispatch of `$this->configure()` from the parent constructor. + pub fn new(name: Option) -> Self { + let mut this = CommandData { application: None, name: None, process_title: None, aliases: Vec::new(), - definition: Some(InputDefinition::new(Vec::new())?), + definition: Some( + InputDefinition::new(Vec::new()).expect("an empty InputDefinition cannot fail"), + ), hidden: false, help: String::new(), description: String::new(), @@ -87,204 +93,374 @@ impl BaseCommand { helper_set: None, }; - let mut name = name; - if name.is_none() { - name = Self::get_default_name(); - if let Some(n) = name.clone() { - let mut aliases: Vec = n.split('|').map(|s| s.to_string()).collect(); - - let first = if aliases.is_empty() { - None - } else { - Some(aliases.remove(0)) - }; - name = first; - if name.as_deref() == Some("") { - this.set_hidden(true); - name = if aliases.is_empty() { - None - } else { - Some(aliases.remove(0)) - }; - } + // PHP's __construct also derives the name from getDefaultName() when null and + // sets the default description; both rely on Reflection late-static-binding + // (get_default_name/get_default_description are todo!()), and concrete commands + // always set their name in configure(), so only an explicit name is honored here. + if let Some(name) = name { + this.name = Some(name); + } - this.set_aliases(aliases)?; - } + this + } + + /// Applies a `$defaultName`-style name (PHP `Command::__construct` when `$name` is null and a + /// `static $defaultName` exists). The string is `|`-separated; a leading empty segment marks the + /// command hidden, the next segment is the name, and the rest are aliases. + pub fn apply_default_name(&mut self, default_name: &str) -> anyhow::Result<()> { + let mut aliases: Vec = default_name.split('|').map(|s| s.to_string()).collect(); + let mut name = aliases.remove(0); + if name.is_empty() { + self.set_hidden(true); + name = if aliases.is_empty() { + String::new() + } else { + aliases.remove(0) + }; } + self.set_name(&name)?; + self.set_aliases(aliases)?; + Ok(()) + } - if let Some(n) = name { - this.set_name(&n)?; + /// Validates a command name. + /// + /// It must be non-empty and parts can optionally be separated by ":". + /// + /// Throws InvalidArgumentException when the name is invalid. + fn validate_name(&self, name: &str) -> anyhow::Result> { + let mut matches: Vec> = Vec::new(); + if !shirabe_php_shim::preg_match(r"/^[^\:]++(\:[^\:]++)*$/", name, &mut matches) { + return Ok(Err(InvalidArgumentException( + shirabe_php_shim::InvalidArgumentException { + message: format!("Command name \"{}\" is invalid.", name), + code: 0, + }, + ))); } - if this.description.is_empty() { - this.set_description(&Self::get_default_description().unwrap_or_default()); + Ok(Ok(())) + } + + /// Sets an array of argument and option instances (the Symfony-typed entry point; + /// `BaseCommand::set_definition` adapts the Composer-typed arguments to this). + pub fn set_definition(&mut self, definition: SetDefinitionArg) -> &mut Self { + match definition { + SetDefinitionArg::Definition(definition) => { + self.definition = Some(definition); + } + SetDefinitionArg::Array(definition) => { + let _ = self.definition.as_mut().unwrap().set_definition(definition); + } } - this.configure(); + self.full_definition = None; - Ok(this) + self } - /// Ignores validation errors. + /// Adds an argument (Symfony-typed entry point). /// - /// This is mainly useful for the help command. - pub fn ignore_validation_errors(&mut self) { - self.ignore_validation_errors = true; + /// Throws InvalidArgumentException when argument mode is not valid. + pub fn add_argument( + &mut self, + name: &str, + mode: Option, + description: &str, + default: PhpMixed, + ) -> anyhow::Result<&mut Self> { + self.definition + .as_mut() + .unwrap() + .add_argument(InputArgument::new( + name.to_string(), + mode, + description.to_string(), + default.clone(), + )?)?; + if self.full_definition.is_some() { + self.full_definition + .as_mut() + .unwrap() + .add_argument(InputArgument::new( + name.to_string(), + mode, + description.to_string(), + default, + )?)?; + } + + Ok(self) } - pub fn set_application(&mut self, application: Option>>) { - self.application = application.clone(); - if let Some(application) = application { - self.set_helper_set(application.borrow_mut().get_helper_set()); - } else { - self.helper_set = None; + /// Adds an option (Symfony-typed entry point). + /// + /// Throws InvalidArgumentException if option mode is invalid or incompatible. + pub fn add_option( + &mut self, + name: &str, + shortcut: PhpMixed, + mode: Option, + description: &str, + default: PhpMixed, + ) -> anyhow::Result<&mut Self> { + self.definition + .as_mut() + .unwrap() + .add_option(InputOption::new( + name, + shortcut.clone(), + mode, + description.to_string(), + default.clone(), + )?)?; + if self.full_definition.is_some() { + self.full_definition + .as_mut() + .unwrap() + .add_option(InputOption::new( + name, + shortcut, + mode, + description.to_string(), + default, + )?)?; } - self.full_definition = None; + Ok(self) } +} - pub fn set_helper_set(&mut self, helper_set: Rc>) { - self.helper_set = Some(helper_set); - } +/// The argument of `CommandData::set_definition()`, which accepts either an array of +/// argument/option instances or an InputDefinition. +#[derive(Debug)] +pub enum SetDefinitionArg { + Array(Vec), + Definition(InputDefinition), +} - /// Gets the helper set. - pub fn get_helper_set(&self) -> Option>> { - self.helper_set.clone() - } +/// Forwards a single trait method to an embedded field that already implements the +/// method (the "inner" command-state holder). +/// +/// Each `Command`/`BaseCommand` implementer spells out the methods it delegates, one +/// `delegate_to_inner!` per method, alongside the few methods it overrides by hand. +/// The first argument names the field to forward to; the second is the method's +/// signature. Fluent setters returning `&mut Self` (optionally wrapped in +/// `anyhow::Result`) are handled specially so the returned reference is re-rooted at +/// the outer `self` rather than the inner field. +#[macro_export] +macro_rules! delegate_to_inner { + // fluent fallible: -> anyhow::Result<&mut Self> + ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> anyhow::Result<&mut Self>) => { + fn $name(&mut self $(, $arg: $ty)*) -> anyhow::Result<&mut Self> { + self.$field.$name($($arg),*)?; + Ok(self) + } + }; + // fluent infallible: -> &mut Self + ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> &mut Self) => { + fn $name(&mut self $(, $arg: $ty)*) -> &mut Self { + self.$field.$name($($arg),*); + self + } + }; + // &self with a return type + ($field:ident, fn $name:ident(&self $(, $arg:ident : $ty:ty )* $(,)?) -> $ret:ty) => { + fn $name(&self $(, $arg: $ty)*) -> $ret { + self.$field.$name($($arg),*) + } + }; + // &self without a return type + ($field:ident, fn $name:ident(&self $(, $arg:ident : $ty:ty )* $(,)?)) => { + fn $name(&self $(, $arg: $ty)*) { + self.$field.$name($($arg),*) + } + }; + // &mut self with a return type + ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> $ret:ty) => { + fn $name(&mut self $(, $arg: $ty)*) -> $ret { + self.$field.$name($($arg),*) + } + }; + // &mut self without a return type + ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?)) => { + fn $name(&mut self $(, $arg: $ty)*) { + self.$field.$name($($arg),*) + } + }; +} - /// Gets the application instance for this command. - pub fn get_application(&self) -> Option>> { - self.application.clone() - } +/// Forwards every `Command` state method (the setters/getters whose canonical impl lives on +/// `CommandData` and which no subclass overrides) to an embedded field. Each command invokes +/// this once inside its `impl Command` block and spells out by hand only the behavior hooks it +/// overrides (`configure`/`execute`/`initialize`/...). The single argument names the field to +/// forward to (`inner` for Symfony commands, `base_command_data` for Composer commands). +#[macro_export] +macro_rules! delegate_command_trait_impls_to_inner { + ($field:ident) => { + $crate::delegate_to_inner!($field, fn is_enabled(&self) -> bool); + $crate::delegate_to_inner!($field, fn set_application(&mut self, application: Option>>)); + $crate::delegate_to_inner!($field, fn get_application(&self) -> Option>>); + $crate::delegate_to_inner!($field, fn set_helper_set(&mut self, helper_set: std::rc::Rc>)); + $crate::delegate_to_inner!($field, fn get_helper_set(&self) -> Option>>); + $crate::delegate_to_inner!($field, fn merge_application_definition(&mut self, merge_args: bool)); + $crate::delegate_to_inner!($field, fn get_definition(&self) -> &$crate::symfony::console::input::input_definition::InputDefinition); + $crate::delegate_to_inner!($field, fn get_native_definition(&self) -> &$crate::symfony::console::input::input_definition::InputDefinition); + $crate::delegate_to_inner!($field, fn set_name(&mut self, name: &str) -> anyhow::Result<()>); + $crate::delegate_to_inner!($field, fn get_name(&self) -> Option); + $crate::delegate_to_inner!($field, fn set_process_title(&mut self, title: &str)); + $crate::delegate_to_inner!($field, fn get_process_title(&self) -> Option); + $crate::delegate_to_inner!($field, fn set_hidden(&mut self, hidden: bool)); + $crate::delegate_to_inner!($field, fn is_hidden(&self) -> bool); + $crate::delegate_to_inner!($field, fn set_description(&mut self, description: &str)); + $crate::delegate_to_inner!($field, fn get_description(&self) -> String); + $crate::delegate_to_inner!($field, fn set_help(&mut self, help: &str)); + $crate::delegate_to_inner!($field, fn get_help(&self) -> String); + $crate::delegate_to_inner!($field, fn get_processed_help(&self) -> String); + $crate::delegate_to_inner!($field, fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()>); + $crate::delegate_to_inner!($field, fn get_aliases(&self) -> Vec); + $crate::delegate_to_inner!($field, fn get_synopsis(&mut self, short: bool) -> String); + $crate::delegate_to_inner!($field, fn add_usage(&mut self, usage: &str)); + $crate::delegate_to_inner!($field, fn get_usages(&self) -> Vec); + $crate::delegate_to_inner!($field, fn get_helper(&self, name: &str) -> anyhow::Result>); + $crate::delegate_to_inner!($field, fn set_code(&mut self, code: Box shirabe_php_shim::PhpMixed>)); + $crate::delegate_to_inner!($field, fn get_code(&self) -> Option<&Box shirabe_php_shim::PhpMixed>>); + $crate::delegate_to_inner!($field, fn ignore_validation_errors(&mut self)); + $crate::delegate_to_inner!($field, fn get_ignore_validation_errors(&self) -> bool); + }; +} - /// Checks whether the command is enabled or not in the current environment. - /// - /// Override this to check for x or y and return false if the command cannot - /// run properly under the current conditions. - pub fn is_enabled(&self) -> bool { - true +/// Polymorphic interface for all commands (PHP's `Command` base class as seen by +/// callers that hold a command of unknown concrete type). +/// +/// The canonical behavior lives in `impl Command for CommandData`; subclasses forward +/// the state methods there and override the behavior hooks (`configure`/`execute`/...). +/// Object-safe so `dyn Command` works; the fluent `where Self: Sized` setters are only +/// called from `configure()` on a concrete command, never through `dyn Command`. +pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny { + fn clone_box(&self) -> Box { + todo!() } + // --- behavior hooks (PHP-overridable; defaults match the PHP `Command` class) --- + /// Configures the current command. - pub fn configure(&mut self) {} + fn configure(&mut self) -> anyhow::Result<()> { + Ok(()) + } - /// Executes the current command. + /// Executes the current command, returning 0 or an exit code. /// - /// This method is not abstract because you can use this class - /// as a concrete class. In this case, instead of defining the - /// execute() method, you set the code to execute by passing - /// a Closure to the set_code() method. - /// - /// Returns 0 if everything went fine, or an exit code. - /// - /// Throws LogicException when this abstract method is not implemented. - pub fn execute( + /// Concrete commands override this; reaching the default means a command class + /// forgot to implement it (PHP throws LogicException — a programming error here). + fn execute( &mut self, - _input: &mut dyn InputInterface, - _output: &mut dyn OutputInterface, - ) -> anyhow::Result> { - Ok(Err(LogicException(shirabe_php_shim::LogicException { - message: "You must override the execute() method in the concrete command class." - .to_string(), - code: 0, - }))) + _input: Rc>, + _output: Rc>, + ) -> anyhow::Result { + panic!("You must override the execute() method in the concrete command class."); } - /// Interacts with the user. - /// - /// This method is executed before the InputDefinition is validated. - /// This means that this is the only place where the command can - /// interactively ask for values of missing required arguments. - pub fn interact(&mut self, _input: &mut dyn InputInterface, _output: &mut dyn OutputInterface) { + /// Interacts with the user before the InputDefinition is validated. + fn interact( + &mut self, + _input: Rc>, + _output: Rc>, + ) { } - /// Initializes the command after the input has been bound and before the input - /// is validated. - /// - /// This is mainly useful when a lot of commands extends one main command - /// where some things need to be initialized based on the input arguments and options. - pub fn initialize( + /// Initializes the command after the input has been bound and before it is validated. + fn initialize( &mut self, - _input: &mut dyn InputInterface, - _output: &mut dyn OutputInterface, - ) { + _input: Rc>, + _output: Rc>, + ) -> anyhow::Result<()> { + Ok(()) + } + + /// Adds suggestions to `suggestions` for the current completion input. + fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) {} + + /// Whether this command proxies to another application/command (Composer's + /// `BaseCommand::isProxyCommand`). Exposed here so the `dyn Command` registry can detect proxy + /// commands without downcasting to the Composer `BaseCommand` trait; defaults to `false` and is + /// overridden by Composer proxy commands such as `GlobalCommand`. + fn is_proxy_command(&self) -> bool { + false } /// Runs the command. /// - /// The code to execute is either defined directly with the - /// set_code() method or by overriding the execute() method - /// in a sub-class. - /// - /// Returns the command exit code. - /// - /// Throws ExceptionInterface when input binding fails. Bypass this by calling ignore_validation_errors(). - pub fn run( + /// Template method: it calls `self.initialize()`, `self.interact()` and + /// `self.execute()`, which dispatch to the concrete command's overrides. It must + /// not be overridden (except by proxy commands like `GlobalCommand`) nor delegated, + /// or that late binding breaks. + fn run( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc>, + output: Rc>, ) -> anyhow::Result { // add the application arguments and options self.merge_application_definition(true); // bind the input against the command specific arguments/options - match input.bind(self.get_definition()) { + match input.borrow_mut().bind(self.get_definition()) { Ok(()) => {} Err(e) => { - if !self.ignore_validation_errors { + if !self.get_ignore_validation_errors() { return Err(e); } } } - self.initialize(input, output); + self.initialize(input.clone(), output.clone())?; - if let Some(process_title) = &self.process_title { + if let Some(process_title) = self.get_process_title() { // TODO: PHP probes for cli_set_process_title / setproctitle availability. if shirabe_php_shim::function_exists("cli_set_process_title") { - if !shirabe_php_shim::cli_set_process_title(process_title) { + if !shirabe_php_shim::cli_set_process_title(&process_title) { if shirabe_php_shim::PHP_OS == "Darwin" { - output.writeln( + output.borrow_mut().writeln( &["Running \"cli_set_process_title\" as an unprivileged user is not supported on MacOS.".to_string()], output_interface::VERBOSITY_VERY_VERBOSE, ); } else { - shirabe_php_shim::cli_set_process_title(process_title); + shirabe_php_shim::cli_set_process_title(&process_title); } } } else if shirabe_php_shim::function_exists("setproctitle") { - shirabe_php_shim::setproctitle(process_title); - } else if output.get_verbosity() == output_interface::VERBOSITY_VERY_VERBOSE { - output.writeln( + shirabe_php_shim::setproctitle(&process_title); + } else if output.borrow().get_verbosity() == output_interface::VERBOSITY_VERY_VERBOSE { + output.borrow_mut().writeln( &["Install the proctitle PECL to be able to change the process title.".to_string()], output_interface::OUTPUT_NORMAL, ); } } - if input.is_interactive() { - self.interact(input, output); + if input.borrow().is_interactive() { + self.interact(input.clone(), output.clone()); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. - if input.has_argument("command") && matches!(input.get_argument("command")?, PhpMixed::Null) + if input.borrow().has_argument("command") + && matches!(input.borrow().get_argument("command")?, PhpMixed::Null) { - input.set_argument("command", PhpMixed::from(self.get_name()))?; + let name = self.get_name(); + input + .borrow_mut() + .set_argument("command", PhpMixed::from(name))?; } - input.validate()?; + input.borrow_mut().validate()?; let status_code: PhpMixed; - if let Some(code) = &self.code { - status_code = code(input, output); + if let Some(code) = self.get_code() { + status_code = code(&mut *input.borrow_mut(), &mut *output.borrow_mut()); } else { - let executed = self.execute(input, output)?; - let executed = match executed { - Ok(v) => v, - Err(e) => return Err(anyhow::Error::new(e)), - }; + let executed = self.execute(input.clone(), output.clone())?; status_code = PhpMixed::from(executed); // PHP also raises \TypeError when execute() does not return int; in this // strongly-typed port execute() already returns an int, so the check is moot. @@ -294,87 +470,192 @@ impl BaseCommand { Ok(shirabe_php_shim::is_numeric_to_int(&status_code)) } - /// Adds suggestions to `suggestions` for the current completion input (e.g. option or argument). - pub fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) {} + // --- state methods (canonical impl on `CommandData`; subclasses forward there) --- - /// Sets the code to execute when running this command. - /// - /// If this method is used, it overrides the code defined - /// in the execute() method. - /// - /// `$code` is a callable(InputInterface, OutputInterface). - /// - /// Throws InvalidArgumentException. - pub fn set_code( + fn is_enabled(&self) -> bool; + + fn set_application(&mut self, application: Option>>); + + fn get_application(&self) -> Option>>; + + fn set_helper_set(&mut self, helper_set: Rc>); + + fn get_helper_set(&self) -> Option>>; + + fn merge_application_definition(&mut self, merge_args: bool); + + fn get_definition(&self) -> &InputDefinition; + + fn get_native_definition(&self) -> &InputDefinition; + + fn set_name(&mut self, name: &str) -> anyhow::Result<()>; + + fn get_name(&self) -> Option; + + fn set_process_title(&mut self, title: &str); + + fn get_process_title(&self) -> Option; + + fn set_hidden(&mut self, hidden: bool); + + fn is_hidden(&self) -> bool; + + fn set_description(&mut self, description: &str); + + fn get_description(&self) -> String; + + fn set_help(&mut self, help: &str); + + fn get_help(&self) -> String; + + fn get_processed_help(&self) -> String; + + fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()>; + + fn get_aliases(&self) -> Vec; + + fn get_synopsis(&mut self, short: bool) -> String; + + fn add_usage(&mut self, usage: &str); + + fn get_usages(&self) -> Vec; + + fn get_helper( + &self, + name: &str, + ) -> anyhow::Result< + Result, + >; + + fn set_code( &mut self, code: Box PhpMixed>, - ) -> &mut Self { - // TODO: PHP rebinds an unbound Closure's $this to the command instance via - // ReflectionFunction/Closure::bind. Rust closures have no `$this` rebinding; - // the closure is stored as-is. - self.code = Some(code); + ); - self + fn get_code( + &self, + ) -> Option<&Box PhpMixed>>; + + fn ignore_validation_errors(&mut self); + + fn get_ignore_validation_errors(&self) -> bool; +} + +impl Command for CommandData { + fn is_enabled(&self) -> bool { + true + } + + fn set_application(&mut self, application: Option>>) { + self.application = application.clone(); + if let Some(application) = application { + self.set_helper_set(application.borrow_mut().get_helper_set()); + } else { + self.helper_set = None; + } + + self.full_definition = None; + } + + fn get_application(&self) -> Option>> { + self.application.clone() + } + + fn set_helper_set(&mut self, helper_set: Rc>) { + self.helper_set = Some(helper_set); + } + + fn get_helper_set(&self) -> Option>> { + self.helper_set.clone() } /// Merges the application definition with the command definition. - /// - /// This method is not part of public API and should not be used directly. - /// - /// `$mergeArgs` is whether to merge or not the Application definition arguments to Command definition arguments. - pub fn merge_application_definition(&mut self, merge_args: bool) { - let _application = match &self.application { + fn merge_application_definition(&mut self, merge_args: bool) { + let application = match &self.application { None => return, Some(application) => application.clone(), }; - // TODO: InputDefinition stores options/arguments as `Rc` / - // `Rc` but its setters (`set_options`/`set_arguments`) take owned - // `Vec` / `Vec`. Merging the application and command - // definitions therefore requires an agreed-upon ownership model for the - // definition entries (Phase C). Left as todo!() pending that design. - let _ = merge_args; - todo!() - } + // InputDefinition stores its entries as `Rc` / `Rc` while the + // setters take owned values, so the shared entries are cloned out (both types derive Clone). + let app_definition = application.borrow_mut().get_definition(); - /// Sets an array of argument and option instances. - /// - /// `$definition` is an array of argument and option instances or a definition instance. - pub fn set_definition(&mut self, definition: SetDefinitionArg) -> &mut Self { - match definition { - SetDefinitionArg::Definition(definition) => { - self.definition = Some(definition); - } - SetDefinitionArg::Array(definition) => { - let _ = self.definition.as_mut().unwrap().set_definition(definition); - } - } + let mut full_definition = + InputDefinition::new(Vec::new()).expect("an empty InputDefinition cannot fail"); - self.full_definition = None; + let own_options: Vec = self + .definition + .as_ref() + .unwrap() + .get_options() + .values() + .map(|option| (**option).clone()) + .collect(); + full_definition + .set_options(own_options) + .expect("the command's own options are already valid"); + + let app_options: Vec = app_definition + .borrow() + .get_options() + .values() + .map(|option| (**option).clone()) + .collect(); + full_definition + .add_options(app_options) + .expect("merging the application options cannot conflict here"); + + if merge_args { + let app_arguments: Vec = app_definition + .borrow() + .get_arguments() + .values() + .map(|argument| (**argument).clone()) + .collect(); + full_definition + .set_arguments(app_arguments) + .expect("the application arguments are already valid"); + + let own_arguments: Vec = self + .definition + .as_ref() + .unwrap() + .get_arguments() + .values() + .map(|argument| (**argument).clone()) + .collect(); + full_definition + .add_arguments(Some(own_arguments)) + .expect("merging the command's own arguments cannot conflict here"); + } else { + let own_arguments: Vec = self + .definition + .as_ref() + .unwrap() + .get_arguments() + .values() + .map(|argument| (**argument).clone()) + .collect(); + full_definition + .set_arguments(own_arguments) + .expect("the command's own arguments are already valid"); + } - self + self.full_definition = Some(full_definition); } - /// Gets the InputDefinition attached to this Command. - pub fn get_definition(&self) -> &InputDefinition { + fn get_definition(&self) -> &InputDefinition { match &self.full_definition { Some(full_definition) => full_definition, None => self.get_native_definition(), } } - /// Gets the InputDefinition to be used to create representations of this Command. - /// - /// Can be overridden to provide the original command representation when it would otherwise - /// be changed by merging with the application InputDefinition. - /// - /// This method is not part of public API and should not be used directly. - pub fn get_native_definition(&self) -> &InputDefinition { + fn get_native_definition(&self) -> &InputDefinition { match &self.definition { None => { - // TODO(review): PHP throws LogicException here, but get_native_definition() - // returns InputDefinition (no Result). In this port `definition` is set in - // the constructor, so None should not occur; treated as a programming error. + // PHP throws LogicException; `definition` is set in `new()`, so None is a + // programming error (forgot to call the parent constructor). panic!( "Command class is not correctly initialized. You probably forgot to call the parent constructor." ); @@ -383,156 +664,53 @@ impl BaseCommand { } } - /// Adds an argument. - /// - /// `$mode` is the argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL. - /// `$default` is the default value (for InputArgument::OPTIONAL mode only). - /// - /// Throws InvalidArgumentException when argument mode is not valid. - pub fn add_argument( - &mut self, - name: &str, - mode: Option, - description: &str, - default: PhpMixed, - ) -> anyhow::Result<&mut Self> { - self.definition - .as_mut() - .unwrap() - .add_argument(InputArgument::new( - name.to_string(), - mode, - description.to_string(), - default.clone(), - )?)?; - if self.full_definition.is_some() { - self.full_definition - .as_mut() - .unwrap() - .add_argument(InputArgument::new( - name.to_string(), - mode, - description.to_string(), - default, - )?)?; - } - - Ok(self) - } - - /// Adds an option. - /// - /// `$shortcut` is the shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts. - /// `$mode` is the option mode: One of the InputOption::VALUE_* constants. - /// `$default` is the default value (must be null for InputOption::VALUE_NONE). - /// - /// Throws InvalidArgumentException if option mode is invalid or incompatible. - pub fn add_option( - &mut self, - name: &str, - shortcut: PhpMixed, - mode: Option, - description: &str, - default: PhpMixed, - ) -> anyhow::Result<&mut Self> { - self.definition - .as_mut() - .unwrap() - .add_option(InputOption::new( - name, - shortcut.clone(), - mode, - description.to_string(), - default.clone(), - )?)?; - if self.full_definition.is_some() { - self.full_definition - .as_mut() - .unwrap() - .add_option(InputOption::new( - name, - shortcut, - mode, - description.to_string(), - default, - )?)?; - } - - Ok(self) - } - - /// Sets the name of the command. - /// - /// This method can set both the namespace and the name if - /// you separate them by a colon (:) - /// - /// command.set_name("foo:bar"); - /// - /// Throws InvalidArgumentException when the name is invalid. - pub fn set_name(&mut self, name: &str) -> anyhow::Result<&mut Self> { + fn set_name(&mut self, name: &str) -> anyhow::Result<()> { if let Err(e) = self.validate_name(name)? { return Err(e.into()); } self.name = Some(name.to_string()); - Ok(self) + Ok(()) } - /// Sets the process title of the command. - /// - /// This feature should be used only when creating a long process command, - /// like a daemon. - pub fn set_process_title(&mut self, title: &str) -> &mut Self { - self.process_title = Some(title.to_string()); + fn get_name(&self) -> Option { + self.name.clone() + } - self + fn set_process_title(&mut self, title: &str) { + self.process_title = Some(title.to_string()); } - /// Returns the command name. - pub fn get_name(&self) -> Option { - self.name.clone() + fn get_process_title(&self) -> Option { + self.process_title.clone() } - /// `$hidden` is whether or not the command should be hidden from the list of commands. - pub fn set_hidden(&mut self, hidden: bool) -> &mut Self { + fn set_hidden(&mut self, hidden: bool) { self.hidden = hidden; - - self } - /// Returns whether the command should be publicly shown or not. - pub fn is_hidden(&self) -> bool { + fn is_hidden(&self) -> bool { self.hidden } - /// Sets the description for the command. - pub fn set_description(&mut self, description: &str) -> &mut Self { + fn set_description(&mut self, description: &str) { self.description = description.to_string(); - - self } - /// Returns the description for the command. - pub fn get_description(&self) -> String { + fn get_description(&self) -> String { self.description.clone() } - /// Sets the help for the command. - pub fn set_help(&mut self, help: &str) -> &mut Self { + fn set_help(&mut self, help: &str) { self.help = help.to_string(); - - self } - /// Returns the help for the command. - pub fn get_help(&self) -> String { + fn get_help(&self) -> String { self.help.clone() } - /// Returns the processed help for the command replacing the %command.name% and - /// %command.full_name% patterns with the real values dynamically. - pub fn get_processed_help(&self) -> String { + fn get_processed_help(&self) -> String { let name = self.name.clone(); let is_single_command = match &self.application { Some(application) => application.borrow().is_single_command(), @@ -563,12 +741,7 @@ impl BaseCommand { shirabe_php_shim::str_replace_array(&placeholders, &replacements, &subject) } - /// Sets the aliases for the command. - /// - /// `$aliases` is an array of aliases for the command. - /// - /// Throws InvalidArgumentException when an alias is invalid. - pub fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<&mut Self> { + fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()> { let mut list = Vec::new(); for alias in &aliases { @@ -582,18 +755,14 @@ impl BaseCommand { // array (Vec), so the result is `aliases`; `list` mirrors the validation loop. self.aliases = aliases; - Ok(self) + Ok(()) } - /// Returns the aliases for the command. - pub fn get_aliases(&self) -> Vec { + fn get_aliases(&self) -> Vec { self.aliases.clone() } - /// Returns the synopsis for the command. - /// - /// `$short` is whether to show the short version of the synopsis (with options folded) or not. - pub fn get_synopsis(&mut self, short: bool) -> String { + fn get_synopsis(&mut self, short: bool) -> String { let key = if short { "short" } else { "long" }.to_string(); if !self.synopsis.contains_key(&key) { @@ -610,8 +779,7 @@ impl BaseCommand { self.synopsis[&key].clone() } - /// Add a command usage example, it'll be prefixed with the command name. - pub fn add_usage(&mut self, usage: &str) -> &mut Self { + fn add_usage(&mut self, usage: &str) { let mut usage = usage.to_string(); let name = self.name.clone().unwrap_or_default(); if !usage.starts_with(&name) { @@ -619,29 +787,31 @@ impl BaseCommand { } self.usages.push(usage); - - self } - /// Returns alternative usages of the command. - pub fn get_usages(&self) -> Vec { + fn get_usages(&self) -> Vec { self.usages.clone() } - /// Gets a helper instance by name. - /// - /// Throws LogicException if no HelperSet is defined. - /// Throws InvalidArgumentException if the helper is not defined. - pub fn get_helper(&self, name: &str) -> anyhow::Result> { + fn get_helper( + &self, + name: &str, + ) -> anyhow::Result< + Result, + > { let helper_set = match &self.helper_set { None => { - return Ok(Err(LogicException(shirabe_php_shim::LogicException { - message: format!( - "Cannot retrieve helper \"{}\" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.", - name + return Ok(Err( + crate::symfony::console::exception::logic_exception::LogicException( + shirabe_php_shim::LogicException { + message: format!( + "Cannot retrieve helper \"{}\" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.", + name + ), + code: 0, + }, ), - code: 0, - }))); + )); } Some(helper_set) => helper_set, }; @@ -653,158 +823,34 @@ impl BaseCommand { todo!() } - /// Validates a command name. - /// - /// It must be non-empty and parts can optionally be separated by ":". - /// - /// Throws InvalidArgumentException when the name is invalid. - fn validate_name(&self, name: &str) -> anyhow::Result> { - let mut matches: Vec> = Vec::new(); - if !shirabe_php_shim::preg_match(r"/^[^\:]++(\:[^\:]++)*$/", name, &mut matches) { - return Ok(Err(InvalidArgumentException( - shirabe_php_shim::InvalidArgumentException { - message: format!("Command name \"{}\" is invalid.", name), - code: 0, - }, - ))); - } - - Ok(Ok(())) - } -} - -/// The argument of Command::set_definition(), which accepts either an array of -/// argument/option instances or an InputDefinition. -#[derive(Debug)] -pub enum SetDefinitionArg { - Array(Vec), - Definition(InputDefinition), -} - -/// Polymorphic interface for all commands (PHP's `Command` base class as seen by -/// callers that hold a command of unknown concrete type). -/// -/// Phase B: default methods are `todo!()`; the concrete behavior lives on -/// `BaseCommand`'s inherent methods. Object-safe so `dyn Command` works. -pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny { - fn clone_box(&self) -> Box { - todo!() - } - - fn configure(&mut self) { - todo!() - } - - fn run( + fn set_code( &mut self, - _input: &mut dyn InputInterface, - _output: &mut dyn OutputInterface, - ) -> anyhow::Result { - todo!() - } - - fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) { - todo!() - } - - fn is_enabled(&self) -> bool { - todo!() - } - - fn set_application(&mut self, _application: Option>>) { - todo!() - } - - fn get_application(&self) -> Option>> { - todo!() - } - - fn set_helper_set(&mut self, _helper_set: Rc>) { - todo!() - } - - fn get_helper_set(&self) -> Option>> { - todo!() - } - - fn merge_application_definition(&mut self, _merge_args: bool) { - todo!() - } - - fn get_definition(&self) -> &InputDefinition { - todo!() - } - - fn get_native_definition(&self) -> &InputDefinition { - todo!() - } - - fn set_name(&mut self, _name: &str) -> anyhow::Result<()> { - todo!() - } - - fn get_name(&self) -> Option { - todo!() - } - - fn set_hidden(&mut self, _hidden: bool) { - todo!() - } - - fn is_hidden(&self) -> bool { - todo!() - } - - fn set_description(&mut self, _description: &str) { - todo!() - } - - fn get_description(&self) -> String { - todo!() - } - - fn set_help(&mut self, _help: &str) { - todo!() - } - - fn get_help(&self) -> String { - todo!() - } - - fn get_processed_help(&self) -> String { - todo!() - } - - fn set_aliases(&mut self, _aliases: Vec) -> anyhow::Result<()> { - todo!() - } - - fn get_aliases(&self) -> Vec { - todo!() - } - - fn get_synopsis(&mut self, _short: bool) -> String { - todo!() + code: Box PhpMixed>, + ) { + // TODO: PHP rebinds an unbound Closure's $this to the command instance via + // ReflectionFunction/Closure::bind. Rust closures have no `$this` rebinding; + // the closure is stored as-is. + self.code = Some(code); } - fn get_usages(&self) -> Vec { - todo!() + fn get_code( + &self, + ) -> Option<&Box PhpMixed>> { + self.code.as_ref() } - fn get_helper(&self, _name: &str) -> anyhow::Result> { - todo!() + fn ignore_validation_errors(&mut self) { + self.ignore_validation_errors = true; } - fn ignore_validation_errors(&mut self) { - todo!() + fn get_ignore_validation_errors(&self) -> bool { + self.ignore_validation_errors } } -impl Command for BaseCommand {} - -impl std::fmt::Debug for BaseCommand { +impl std::fmt::Debug for CommandData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BaseCommand") + f.debug_struct("CommandData") .field("name", &self.name) .field("aliases", &self.aliases) .field("hidden", &self.hidden) -- cgit v1.3.1