diff options
Diffstat (limited to 'crates/shirabe-external-packages/src')
9 files changed, 987 insertions, 1358 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/application.rs b/crates/shirabe-external-packages/src/symfony/console/application.rs index 2a5fe4a..605851f 100644 --- a/crates/shirabe-external-packages/src/symfony/console/application.rs +++ b/crates/shirabe-external-packages/src/symfony/console/application.rs @@ -12,7 +12,7 @@ use std::rc::Rc; /// `Symfony\Component\Console\Application` is a concrete class in PHP, but it is ported here as a /// trait rather than a struct. /// Refer to shirabe::console::Application for the reason. -pub trait Application: std::fmt::Debug { +pub trait Application: std::fmt::Debug + shirabe_php_shim::AsAny { fn get_name(&self) -> String; fn get_version(&self) -> String; 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<Rc<RefCell<dyn Application>>>, name: Option<String>, process_title: Option<String>, @@ -39,7 +40,7 @@ pub struct BaseCommand { helper_set: Option<Rc<RefCell<HelperSet>>>, } -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<String>) -> anyhow::Result<Self> { - 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<String>) -> 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<String> = n.split('|').map(|s| s.to_string()).collect(); + // 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); + } - 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)) - }; - } + this + } - this.set_aliases(aliases)?; - } + /// 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<String> = 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<Result<(), InvalidArgumentException>> { + let mut matches: Vec<Option<String>> = 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<i64>, + 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<Rc<RefCell<dyn Application>>>) { - 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<i64>, + 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<RefCell<HelperSet>>) { - 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<crate::symfony::console::input::input_definition::DefinitionItem>), + Definition(InputDefinition), +} - /// Gets the helper set. - pub fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { - 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<Rc<RefCell<dyn Application>>> { - 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<std::rc::Rc<std::cell::RefCell<dyn $crate::symfony::console::application::Application>>>)); + $crate::delegate_to_inner!($field, fn get_application(&self) -> Option<std::rc::Rc<std::cell::RefCell<dyn $crate::symfony::console::application::Application>>>); + $crate::delegate_to_inner!($field, fn set_helper_set(&mut self, helper_set: std::rc::Rc<std::cell::RefCell<$crate::symfony::console::helper::helper_set::HelperSet>>)); + $crate::delegate_to_inner!($field, fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<$crate::symfony::console::helper::helper_set::HelperSet>>>); + $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<String>); + $crate::delegate_to_inner!($field, fn set_process_title(&mut self, title: &str)); + $crate::delegate_to_inner!($field, fn get_process_title(&self) -> Option<String>); + $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<String>) -> anyhow::Result<()>); + $crate::delegate_to_inner!($field, fn get_aliases(&self) -> Vec<String>); + $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<String>); + $crate::delegate_to_inner!($field, fn get_helper(&self, name: &str) -> anyhow::Result<Result<shirabe_php_shim::PhpMixed, $crate::symfony::console::exception::logic_exception::LogicException>>); + $crate::delegate_to_inner!($field, fn set_code(&mut self, code: Box<dyn Fn(&mut dyn $crate::symfony::console::input::InputInterface, &mut dyn $crate::symfony::console::output::OutputInterface) -> shirabe_php_shim::PhpMixed>)); + $crate::delegate_to_inner!($field, fn get_code(&self) -> Option<&Box<dyn Fn(&mut dyn $crate::symfony::console::input::InputInterface, &mut dyn $crate::symfony::console::output::OutputInterface) -> 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<dyn Command> { + 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. - /// - /// 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. + /// Executes the current command, returning 0 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<Result<i64, LogicException>> { - 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<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + 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<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) { } - /// 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<RefCell<dyn InputInterface>>, + _output: Rc<RefCell<dyn OutputInterface>>, + ) -> 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<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { // 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( &["<comment>Running \"cli_set_process_title\" as an unprivileged user is not supported on MacOS.</comment>".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( &["<comment>Install the proctitle PECL to be able to change the process title.</comment>".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<Rc<RefCell<dyn Application>>>); + + fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>>; + + fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>); + + fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>>; + + 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<String>; + + fn set_process_title(&mut self, title: &str); + + fn get_process_title(&self) -> Option<String>; + + 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<String>) -> anyhow::Result<()>; + + fn get_aliases(&self) -> Vec<String>; + + fn get_synopsis(&mut self, short: bool) -> String; + + fn add_usage(&mut self, usage: &str); + + fn get_usages(&self) -> Vec<String>; + + fn get_helper( + &self, + name: &str, + ) -> anyhow::Result< + Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, + >; + + fn set_code( &mut self, code: Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> 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<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> 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<Rc<RefCell<dyn Application>>>) { + 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<Rc<RefCell<dyn Application>>> { + self.application.clone() + } + + fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>) { + self.helper_set = Some(helper_set); + } + + fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { + 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<InputOption>` / - // `Rc<InputArgument>` but its setters (`set_options`/`set_arguments`) take owned - // `Vec<InputOption>` / `Vec<InputArgument>`. 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<InputArgument>` / `Rc<InputOption>` 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<InputOption> = 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"); - self + let app_options: Vec<InputOption> = 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<InputArgument> = 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<InputArgument> = 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<InputArgument> = 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.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<i64>, - 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<i64>, - 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<String> { + 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<String> { - self.name.clone() + fn get_process_title(&self) -> Option<String> { + 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<String>) -> anyhow::Result<&mut Self> { + fn set_aliases(&mut self, aliases: Vec<String>) -> 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<String> { + fn get_aliases(&self) -> Vec<String> { 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<String> { + fn get_usages(&self) -> Vec<String> { 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<Result<PhpMixed, LogicException>> { + fn get_helper( + &self, + name: &str, + ) -> anyhow::Result< + Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, + > { 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<Result<(), InvalidArgumentException>> { - let mut matches: Vec<Option<String>> = 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<crate::symfony::console::input::input_definition::DefinitionItem>), - 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<dyn Command> { - todo!() - } - - fn configure(&mut self) { - todo!() - } - - fn run( + fn set_code( &mut self, - _input: &mut dyn InputInterface, - _output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - todo!() - } - - fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) { - todo!() - } - - fn is_enabled(&self) -> bool { - todo!() - } - - fn set_application(&mut self, _application: Option<Rc<RefCell<dyn Application>>>) { - todo!() - } - - fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> { - todo!() - } - - fn set_helper_set(&mut self, _helper_set: Rc<RefCell<HelperSet>>) { - todo!() - } - - fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { - 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<String> { - 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<String>) -> anyhow::Result<()> { - todo!() - } - - fn get_aliases(&self) -> Vec<String> { - todo!() - } - - fn get_synopsis(&mut self, _short: bool) -> String { - todo!() + code: Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> 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<String> { - todo!() + fn get_code( + &self, + ) -> Option<&Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> PhpMixed>> { + self.code.as_ref() } - fn get_helper(&self, _name: &str) -> anyhow::Result<Result<PhpMixed, LogicException>> { - 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) diff --git a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs index 88a3bba..522bb78 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::ops::{Deref, DerefMut}; use std::rc::Rc; -use crate::symfony::console::command::command::{BaseCommand, Command}; +use crate::symfony::console::command::command::{Command, CommandData}; use crate::symfony::console::command::lazy_command::LazyCommand; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::{ @@ -16,18 +16,18 @@ use crate::symfony::console::completion::completion_suggestions::{ use crate::symfony::console::completion::output::completion_output_interface::CompletionOutputInterface; use crate::symfony::console::input::input_interface::InputInterface; use crate::symfony::console::input::input_option::InputOption; -use crate::symfony::console::output::output_interface::{self, OutputInterface}; +use crate::symfony::console::output::output_interface::OutputInterface; /// Responsible for providing the values to the shell completion. #[derive(Debug)] pub struct CompleteCommand { - inner: BaseCommand, + inner: CommandData, completion_outputs: IndexMap<String, PhpMixed>, is_debug: bool, } impl Deref for CompleteCommand { - type Target = BaseCommand; + type Target = CommandData; fn deref(&self) -> &Self::Target { &self.inner @@ -60,15 +60,108 @@ impl CompleteCommand { ) }); - let this = Self { - inner: BaseCommand::__construct(None)?, + let mut this = Self { + inner: CommandData::new(None), completion_outputs, is_debug: false, }; + // PHP: static $defaultName = '|_complete' / $defaultDescription, applied by the parent + // constructor before configure(). + this.inner.apply_default_name(Self::DEFAULT_NAME)?; + this.inner.set_description(Self::DEFAULT_DESCRIPTION); + this.configure()?; Ok(this) } + fn create_completion_input( + &self, + input: &dyn InputInterface, + ) -> anyhow::Result<CompletionInput> { + let current_index = input.get_option("current")?; + if !current_index.to_bool() || !shirabe_php_shim::ctype_digit(¤t_index.to_string()) { + anyhow::bail!(shirabe_php_shim::RuntimeException { + message: "The \"--current\" option must be set and it must be an integer." + .to_string(), + code: 0, + }); + } + + let tokens: Vec<String> = match input.get_option("input")?.as_list() { + Some(list) => list.iter().map(|v| v.to_string()).collect(), + None => Vec::new(), + }; + let mut completion_input = CompletionInput::from_tokens( + tokens, + current_index.to_string().parse::<i64>().unwrap_or(0), + )?; + + // try { $completionInput->bind(...); } catch (ExceptionInterface $e) {} + let application = self.get_application().unwrap(); + let definition = application.borrow_mut().get_definition(); + let _ = completion_input.bind(&definition.borrow()); + + Ok(completion_input) + } + + fn find_command( + &self, + completion_input: &CompletionInput, + _output: &dyn OutputInterface, + ) -> Option<Rc<RefCell<dyn Command>>> { + // try { ... } catch (CommandNotFoundException $e) {} + let input_name = completion_input.get_first_argument()?; + + let application = self.get_application().unwrap(); + // CommandNotFoundException is caught and swallowed by returning None. + application.borrow_mut().find(&input_name).ok() + } + + fn log(&self, messages: &str) { + self.log_many(vec![messages.to_string()]); + } + + fn log_many(&self, messages: Vec<String>) { + if !self.is_debug { + return; + } + + let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); + shirabe_php_shim::file_put_contents3( + &format!( + "{}/sf_{}.log", + shirabe_php_shim::sys_get_temp_dir(), + command_name + ), + &(messages.join(shirabe_php_shim::PHP_EOL) + shirabe_php_shim::PHP_EOL), + shirabe_php_shim::FILE_APPEND, + ); + } +} + +/// \get_class($command instanceof LazyCommand ? $command->getCommand() : $command) +fn get_class_of_command(command: &Rc<RefCell<dyn Command>>) -> String { + let borrowed = command.borrow(); + let _is_lazy = (*borrowed).as_any().downcast_ref::<LazyCommand>().is_some(); + // TODO: get_class() takes a PhpMixed but the command is a `dyn Command`; reflecting the + // concrete class name of a trait object requires a class-name hook on Command (Phase C). + todo!() +} + +/// $command->getDefinition()->getOptions() +fn get_definition_options(_command: &Rc<RefCell<dyn Command>>) -> Vec<InputOption> { + // TODO: InputDefinition::get_options() returns `&IndexMap<String, Rc<InputOption>>` but + // CompletionSuggestions::suggest_options() takes `Vec<InputOption>`; the option ownership + // model must be reconciled (Phase C). + todo!() +} + +/// new $completionOutput(); +fn instantiate_completion_output(_class: &PhpMixed) -> Box<dyn CompletionOutputInterface> { + todo!() +} + +impl Command for CompleteCommand { fn configure(&mut self) -> anyhow::Result<()> { let shells = self .completion_outputs @@ -109,17 +202,24 @@ impl CompleteCommand { Ok(()) } - fn initialize(&mut self, _input: &dyn InputInterface, _output: &dyn OutputInterface) { + fn initialize( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + let _ = (input, output); self.is_debug = shirabe_php_shim::filter_var( &shirabe_php_shim::getenv("SYMFONY_COMPLETION_DEBUG").unwrap_or_default(), shirabe_php_shim::FILTER_VALIDATE_BOOLEAN, ); + + Ok(()) } fn execute( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { // try { ... } catch (\Throwable $e) { ...; if ($output->isDebug()) { throw $e; } return 2; } let result: anyhow::Result<i64> = (|| { @@ -132,7 +232,7 @@ impl CompleteCommand { // return 126; // } - let shell = input.get_option("shell")?; + let shell = input.borrow().get_option("shell")?; if !shell.to_bool() { anyhow::bail!(shirabe_php_shim::RuntimeException { message: "The \"--shell\" option must be set.".to_string(), @@ -160,7 +260,7 @@ impl CompleteCommand { }); } - let mut completion_input = self.create_completion_input(input)?; + let mut completion_input = self.create_completion_input(&*input.borrow())?; let mut suggestions = CompletionSuggestions::new(); self.log_many(vec![ @@ -176,7 +276,7 @@ impl CompleteCommand { "<info>Messages:</>".to_string(), ]); - let command = self.find_command(&completion_input, output); + let command = self.find_command(&completion_input, &*output.borrow()); match command { None => { self.log(" No command found, completing using the Application class."); @@ -273,7 +373,7 @@ impl CompleteCommand { } } - completion_output.write(&suggestions, output); + completion_output.write(&suggestions, &mut *output.borrow_mut()); Ok(0) })(); @@ -283,7 +383,7 @@ impl CompleteCommand { Err(e) => { self.log_many(vec!["<error>Error!</error>".to_string(), format!("{}", e)]); - if output.is_debug() { + if output.borrow().is_debug() { return Err(e); } @@ -292,214 +392,5 @@ impl CompleteCommand { } } - fn create_completion_input( - &self, - input: &dyn InputInterface, - ) -> anyhow::Result<CompletionInput> { - let current_index = input.get_option("current")?; - if !current_index.to_bool() || !shirabe_php_shim::ctype_digit(¤t_index.to_string()) { - anyhow::bail!(shirabe_php_shim::RuntimeException { - message: "The \"--current\" option must be set and it must be an integer." - .to_string(), - code: 0, - }); - } - - let tokens: Vec<String> = match input.get_option("input")?.as_list() { - Some(list) => list.iter().map(|v| v.to_string()).collect(), - None => Vec::new(), - }; - let mut completion_input = CompletionInput::from_tokens( - tokens, - current_index.to_string().parse::<i64>().unwrap_or(0), - )?; - - // try { $completionInput->bind(...); } catch (ExceptionInterface $e) {} - let application = self.get_application().unwrap(); - let definition = application.borrow_mut().get_definition(); - let _ = completion_input.bind(&definition.borrow()); - - Ok(completion_input) - } - - fn find_command( - &self, - completion_input: &CompletionInput, - _output: &dyn OutputInterface, - ) -> Option<Rc<RefCell<dyn Command>>> { - // try { ... } catch (CommandNotFoundException $e) {} - let input_name = completion_input.get_first_argument()?; - - let application = self.get_application().unwrap(); - // CommandNotFoundException is caught and swallowed by returning None. - application.borrow_mut().find(&input_name).ok() - } - - fn log(&self, messages: &str) { - self.log_many(vec![messages.to_string()]); - } - - fn log_many(&self, messages: Vec<String>) { - if !self.is_debug { - return; - } - - let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); - shirabe_php_shim::file_put_contents3( - &format!( - "{}/sf_{}.log", - shirabe_php_shim::sys_get_temp_dir(), - command_name - ), - &(messages.join(shirabe_php_shim::PHP_EOL) + shirabe_php_shim::PHP_EOL), - shirabe_php_shim::FILE_APPEND, - ); - } -} - -/// \get_class($command instanceof LazyCommand ? $command->getCommand() : $command) -fn get_class_of_command(command: &Rc<RefCell<dyn Command>>) -> String { - let borrowed = command.borrow(); - let _is_lazy = (*borrowed).as_any().downcast_ref::<LazyCommand>().is_some(); - // TODO: get_class() takes a PhpMixed but the command is a `dyn Command`; reflecting the - // concrete class name of a trait object requires a class-name hook on Command (Phase C). - todo!() -} - -/// $command->getDefinition()->getOptions() -fn get_definition_options(_command: &Rc<RefCell<dyn Command>>) -> Vec<InputOption> { - // TODO: InputDefinition::get_options() returns `&IndexMap<String, Rc<InputOption>>` but - // CompletionSuggestions::suggest_options() takes `Vec<InputOption>`; the option ownership - // model must be reconciled (Phase C). - todo!() -} - -/// new $completionOutput(); -fn instantiate_completion_output(_class: &PhpMixed) -> Box<dyn CompletionOutputInterface> { - todo!() -} - -impl Command for CompleteCommand { - fn configure(&mut self) { - let _ = CompleteCommand::configure(self); - } - - fn run( - &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - self.inner.run(input, output) - } - - fn is_enabled(&self) -> bool { - self.inner.is_enabled() - } - - fn set_application( - &mut self, - application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, - ) { - self.inner.set_application(application); - } - - fn get_application( - &self, - ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { - self.inner.get_application() - } - - fn set_helper_set( - &mut self, - helper_set: Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>, - ) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set( - &self, - ) -> Option<Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>> { - self.inner.get_helper_set() - } - - fn merge_application_definition(&mut self, merge_args: bool) { - self.inner.merge_application_definition(merge_args); - } - - fn get_definition(&self) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_definition() - } - - fn get_native_definition( - &self, - ) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_native_definition() - } - - fn set_name(&mut self, name: &str) -> anyhow::Result<()> { - self.inner.set_name(name)?; - Ok(()) - } - - fn get_name(&self) -> Option<String> { - self.inner.get_name() - } - - fn set_hidden(&mut self, hidden: bool) { - self.inner.set_hidden(hidden); - } - - fn is_hidden(&self) -> bool { - self.inner.is_hidden() - } - - fn set_description(&mut self, description: &str) { - self.inner.set_description(description); - } - - fn get_description(&self) -> String { - self.inner.get_description() - } - - fn set_help(&mut self, help: &str) { - self.inner.set_help(help); - } - - fn get_help(&self) -> String { - self.inner.get_help() - } - - fn get_processed_help(&self) -> String { - self.inner.get_processed_help() - } - - fn set_aliases(&mut self, aliases: Vec<String>) -> anyhow::Result<()> { - self.inner.set_aliases(aliases)?; - Ok(()) - } - - fn get_aliases(&self) -> Vec<String> { - self.inner.get_aliases() - } - - fn get_synopsis(&mut self, short: bool) -> String { - self.inner.get_synopsis(short) - } - - fn get_usages(&self) -> Vec<String> { - self.inner.get_usages() - } - - fn get_helper( - &self, - name: &str, - ) -> anyhow::Result< - Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, - > { - self.inner.get_helper(name) - } - - fn ignore_validation_errors(&mut self) { - self.inner.ignore_validation_errors(); - } + crate::delegate_command_trait_impls_to_inner!(inner); } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs index 9604c0a..c8947b8 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -1,6 +1,6 @@ //! ref: composer/vendor/symfony/console/Command/DumpCompletionCommand.php -use crate::symfony::console::command::command::{BaseCommand, Command}; +use crate::symfony::console::command::command::{Command, CommandData}; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::{ CompletionSuggestions, StringOrSuggestion, @@ -17,11 +17,11 @@ use std::rc::Rc; /// Dumps the completion script for the current shell. #[derive(Debug)] pub struct DumpCompletionCommand { - inner: BaseCommand, + inner: CommandData, } impl Deref for DumpCompletionCommand { - type Target = BaseCommand; + type Target = CommandData; fn deref(&self) -> &Self::Target { &self.inner @@ -38,6 +38,23 @@ impl DumpCompletionCommand { pub const DEFAULT_NAME: &'static str = "completion"; pub const DEFAULT_DESCRIPTION: &'static str = "Dump the shell completion script"; + pub fn new() -> Self { + let mut command = DumpCompletionCommand { + inner: CommandData::new(None), + }; + // PHP: static $defaultName = 'completion' / $defaultDescription, applied by the parent + // constructor before configure(). + command + .inner + .apply_default_name(Self::DEFAULT_NAME) + .expect("DumpCompletionCommand default name is valid"); + command.inner.set_description(Self::DEFAULT_DESCRIPTION); + command + .configure() + .expect("DumpCompletionCommand::configure uses static, valid metadata"); + command + } + pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { if input.must_suggest_argument_values_for("shell") { suggestions.suggest_values( @@ -49,6 +66,45 @@ impl DumpCompletionCommand { } } + fn guess_shell() -> String { + shirabe_php_shim::basename(&shirabe_php_shim::server_shell().unwrap_or_default()) + } + + fn tail_debug_log(&self, command_name: &str, _output: &dyn OutputInterface) { + let debug_file = format!( + "{}/sf_{}.log", + shirabe_php_shim::sys_get_temp_dir(), + command_name + ); + if !shirabe_php_shim::file_exists(&debug_file) { + shirabe_php_shim::touch(&debug_file); + } + // TODO: Process::run() expects a `'static` callback, but the PHP closure captures + // `$output` by reference and writes each line to it. Bridging the borrowed `output` + // into a `'static` callback requires shared ownership of the output (Phase C). + todo!() + } + + fn get_supported_shells(&self) -> Vec<String> { + let mut shells = vec![]; + + // foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) + for file in shirabe_php_shim::directory_iterator(&format!( + "{}/../Resources/", + shirabe_php_shim::dir() + )) { + if shirabe_php_shim::str_starts_with(&file.get_basename(), "completion.") + && file.is_file() + { + shells.push(file.get_extension()); + } + } + + shells + } +} + +impl Command for DumpCompletionCommand { fn configure(&mut self) -> anyhow::Result<()> { let full_command = shirabe_php_shim::server_php_self(); let command_name = shirabe_php_shim::basename(&full_command); @@ -85,14 +141,14 @@ impl DumpCompletionCommand { Add this to the end of your shell configuration file (e.g. <info>\"~/.bashrc\"</>):\n\ \n\ \x20\x20\x20\x20<info>eval \"$({full_command} completion bash)\"</>", - )) - .add_argument( + )); + self.inner.add_argument( "shell", Some(InputArgument::OPTIONAL), "The shell type (e.g. \"bash\"), the value of the \"$SHELL\" env var will be used if this is not given", PhpMixed::Null, - )? - .add_option( + )?; + self.inner.add_option( "debug", PhpMixed::Null, Some(InputOption::VALUE_NONE), @@ -105,18 +161,18 @@ impl DumpCompletionCommand { fn execute( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]); - if input.get_option("debug")?.to_bool() { - self.tail_debug_log(&command_name, output); + if input.borrow().get_option("debug")?.to_bool() { + self.tail_debug_log(&command_name, &*output.borrow()); return Ok(0); } - let shell = match input.get_argument("shell")?.as_string() { + let shell = match input.borrow().get_argument("shell")?.as_string() { Some(s) => s.to_string(), None => Self::guess_shell(), }; @@ -132,7 +188,7 @@ impl DumpCompletionCommand { // : $output`. There is no way to test trait membership through `&dyn OutputInterface` // here; OutputInterface would need a downcast hook (Phase C). Writing to `output`. if !shell.is_empty() { - output.writeln( + output.borrow_mut().writeln( &[format!( "<error>Detected shell \"{}\", which is not supported by Symfony shell completion (supported shells: \"{}\").</>", shell, @@ -141,7 +197,7 @@ impl DumpCompletionCommand { output_interface::OUTPUT_NORMAL, ); } else { - output.writeln( + output.borrow_mut().writeln( &[format!( "<error>Shell not detected, Symfony shell completion only supports \"{}\").</>", supported_shells.join("\", \"") @@ -155,7 +211,7 @@ impl DumpCompletionCommand { let application = self.get_application().unwrap(); let version = application.borrow().get_version(); - output.write( + output.borrow_mut().write( &[shirabe_php_shim::str_replace_arrays( &[ "{{ COMMAND_NAME }}".to_string(), @@ -171,169 +227,9 @@ impl DumpCompletionCommand { Ok(0) } - fn guess_shell() -> String { - shirabe_php_shim::basename(&shirabe_php_shim::server_shell().unwrap_or_default()) - } - - fn tail_debug_log(&self, command_name: &str, _output: &dyn OutputInterface) { - let debug_file = format!( - "{}/sf_{}.log", - shirabe_php_shim::sys_get_temp_dir(), - command_name - ); - if !shirabe_php_shim::file_exists(&debug_file) { - shirabe_php_shim::touch(&debug_file); - } - // TODO: Process::run() expects a `'static` callback, but the PHP closure captures - // `$output` by reference and writes each line to it. Bridging the borrowed `output` - // into a `'static` callback requires shared ownership of the output (Phase C). - todo!() - } - - fn get_supported_shells(&self) -> Vec<String> { - let mut shells = vec![]; - - // foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) - for file in shirabe_php_shim::directory_iterator(&format!( - "{}/../Resources/", - shirabe_php_shim::dir() - )) { - if shirabe_php_shim::str_starts_with(&file.get_basename(), "completion.") - && file.is_file() - { - shells.push(file.get_extension()); - } - } - - shells - } -} - -impl Command for DumpCompletionCommand { - fn configure(&mut self) { - let _ = DumpCompletionCommand::configure(self); - } - - fn run( - &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - self.inner.run(input, output) - } - fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { self.complete_impl(input, suggestions); } - fn is_enabled(&self) -> bool { - self.inner.is_enabled() - } - - fn set_application( - &mut self, - application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, - ) { - self.inner.set_application(application); - } - - fn get_application( - &self, - ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { - self.inner.get_application() - } - - fn set_helper_set( - &mut self, - helper_set: Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>, - ) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set( - &self, - ) -> Option<Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>> { - self.inner.get_helper_set() - } - - fn merge_application_definition(&mut self, merge_args: bool) { - self.inner.merge_application_definition(merge_args); - } - - fn get_definition(&self) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_definition() - } - - fn get_native_definition( - &self, - ) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_native_definition() - } - - fn set_name(&mut self, name: &str) -> anyhow::Result<()> { - self.inner.set_name(name)?; - Ok(()) - } - - fn get_name(&self) -> Option<String> { - self.inner.get_name() - } - - fn set_hidden(&mut self, hidden: bool) { - self.inner.set_hidden(hidden); - } - - fn is_hidden(&self) -> bool { - self.inner.is_hidden() - } - - fn set_description(&mut self, description: &str) { - self.inner.set_description(description); - } - - fn get_description(&self) -> String { - self.inner.get_description() - } - - fn set_help(&mut self, help: &str) { - self.inner.set_help(help); - } - - fn get_help(&self) -> String { - self.inner.get_help() - } - - fn get_processed_help(&self) -> String { - self.inner.get_processed_help() - } - - fn set_aliases(&mut self, aliases: Vec<String>) -> anyhow::Result<()> { - self.inner.set_aliases(aliases)?; - Ok(()) - } - - fn get_aliases(&self) -> Vec<String> { - self.inner.get_aliases() - } - - fn get_synopsis(&mut self, short: bool) -> String { - self.inner.get_synopsis(short) - } - - fn get_usages(&self) -> Vec<String> { - self.inner.get_usages() - } - - fn get_helper( - &self, - name: &str, - ) -> anyhow::Result< - Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, - > { - self.inner.get_helper(name) - } - - fn ignore_validation_errors(&mut self) { - self.inner.ignore_validation_errors(); - } + crate::delegate_command_trait_impls_to_inner!(inner); } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs index fb2a8a2..321b640 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs @@ -1,6 +1,6 @@ //! ref: composer/vendor/symfony/console/Command/HelpCommand.php -use crate::symfony::console::command::command::{BaseCommand, Command, SetDefinitionArg}; +use crate::symfony::console::command::command::{Command, CommandData, SetDefinitionArg}; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::{ CompletionSuggestions, StringOrSuggestion, @@ -20,12 +20,12 @@ use std::rc::Rc; /// HelpCommand displays the help for a given command. #[derive(Debug)] pub struct HelpCommand { - inner: BaseCommand, + inner: CommandData, command: Option<Rc<RefCell<dyn Command>>>, } impl Deref for HelpCommand { - type Target = BaseCommand; + type Target = CommandData; fn deref(&self) -> &Self::Target { &self.inner @@ -39,79 +39,21 @@ impl DerefMut for HelpCommand { } impl HelpCommand { - fn configure(&mut self) -> anyhow::Result<()> { - self.inner.ignore_validation_errors(); - - self.inner - .set_name("help")? - .set_definition(SetDefinitionArg::Array(vec![ - DefinitionItem::InputArgument(InputArgument::new( - "command_name".to_string(), - Some(InputArgument::OPTIONAL), - "The command name".to_string(), - PhpMixed::from("help".to_string()), - )?), - DefinitionItem::InputOption(InputOption::new( - "format", - PhpMixed::Null, - Some(InputOption::VALUE_REQUIRED), - "The output format (txt, xml, json, or md)".to_string(), - PhpMixed::from("txt".to_string()), - )?), - DefinitionItem::InputOption(InputOption::new( - "raw", - PhpMixed::Null, - Some(InputOption::VALUE_NONE), - "To output raw command help".to_string(), - PhpMixed::Null, - )?), - ])) - .set_description("Display help for a command") - .set_help( - "The <info>%command.name%</info> command displays help for a given command:\n\ - \n\ - \x20\x20<info>%command.full_name% list</info>\n\ - \n\ - You can also output the help in other formats by using the <comment>--format</comment> option:\n\ - \n\ - \x20\x20<info>%command.full_name% --format=xml list</info>\n\ - \n\ - To display the list of available commands, please use the <info>list</info> command.", - ); - - Ok(()) + pub fn new() -> Self { + let mut command = HelpCommand { + inner: CommandData::new(None), + command: None, + }; + command + .configure() + .expect("HelpCommand::configure uses static, valid metadata"); + command } pub fn set_command(&mut self, command: Rc<RefCell<dyn Command>>) { self.command = Some(command); } - fn execute( - &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - if self.command.is_none() { - let application = self.get_application().unwrap(); - let command_name = input.get_argument("command_name")?.to_string(); - self.command = Some(application.borrow_mut().find(&command_name)?); - } - - let helper = DescriptorHelper::new(); - // TODO: DescriptorHelper::describe2 takes the described object as Option<PhpMixed>, - // but PhpMixed cannot hold a Command. The Command/Application object mixing for - // describe needs a dedicated type (Phase C). - let object: Option<PhpMixed> = todo!(); - let mut options = indexmap::IndexMap::new(); - options.insert("format".to_string(), input.get_option("format")?); - options.insert("raw_text".to_string(), input.get_option("raw")?); - let _ = helper.describe2(output, object, options); - - self.command = None; - - Ok(0) - } - pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { if input.must_suggest_argument_values_for("command_name") { let application = self.get_application().unwrap(); @@ -142,130 +84,77 @@ impl HelpCommand { } impl Command for HelpCommand { - fn configure(&mut self) { - let _ = HelpCommand::configure(self); - } - - fn run( - &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - self.inner.run(input, output) - } - - fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { - self.complete_impl(input, suggestions); - } - - fn is_enabled(&self) -> bool { - self.inner.is_enabled() - } - - fn set_application( - &mut self, - application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, - ) { - self.inner.set_application(application); - } - - fn get_application( - &self, - ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { - self.inner.get_application() - } - - fn set_helper_set( - &mut self, - helper_set: Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>, - ) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set( - &self, - ) -> Option<Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>> { - self.inner.get_helper_set() - } - - fn merge_application_definition(&mut self, merge_args: bool) { - self.inner.merge_application_definition(merge_args); - } - - fn get_definition(&self) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_definition() - } + fn configure(&mut self) -> anyhow::Result<()> { + self.inner.ignore_validation_errors(); - fn get_native_definition( - &self, - ) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_native_definition() - } + self.inner.set_name("help")?; + self.inner.set_definition(SetDefinitionArg::Array(vec![ + DefinitionItem::InputArgument(InputArgument::new( + "command_name".to_string(), + Some(InputArgument::OPTIONAL), + "The command name".to_string(), + PhpMixed::from("help".to_string()), + )?), + DefinitionItem::InputOption(InputOption::new( + "format", + PhpMixed::Null, + Some(InputOption::VALUE_REQUIRED), + "The output format (txt, xml, json, or md)".to_string(), + PhpMixed::from("txt".to_string()), + )?), + DefinitionItem::InputOption(InputOption::new( + "raw", + PhpMixed::Null, + Some(InputOption::VALUE_NONE), + "To output raw command help".to_string(), + PhpMixed::Null, + )?), + ])); + self.inner.set_description("Display help for a command"); + self.inner.set_help( + "The <info>%command.name%</info> command displays help for a given command:\n\ + \n\ + \x20\x20<info>%command.full_name% list</info>\n\ + \n\ + You can also output the help in other formats by using the <comment>--format</comment> option:\n\ + \n\ + \x20\x20<info>%command.full_name% --format=xml list</info>\n\ + \n\ + To display the list of available commands, please use the <info>list</info> command.", + ); - fn set_name(&mut self, name: &str) -> anyhow::Result<()> { - self.inner.set_name(name)?; Ok(()) } - fn get_name(&self) -> Option<String> { - self.inner.get_name() - } - - fn set_hidden(&mut self, hidden: bool) { - self.inner.set_hidden(hidden); - } - - fn is_hidden(&self) -> bool { - self.inner.is_hidden() - } - - fn set_description(&mut self, description: &str) { - self.inner.set_description(description); - } - - fn get_description(&self) -> String { - self.inner.get_description() - } - - fn set_help(&mut self, help: &str) { - self.inner.set_help(help); - } - - fn get_help(&self) -> String { - self.inner.get_help() - } - - fn get_processed_help(&self) -> String { - self.inner.get_processed_help() - } - - fn set_aliases(&mut self, aliases: Vec<String>) -> anyhow::Result<()> { - self.inner.set_aliases(aliases)?; - Ok(()) - } + fn execute( + &mut self, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + if self.command.is_none() { + let application = self.get_application().unwrap(); + let command_name = input.borrow().get_argument("command_name")?.to_string(); + self.command = Some(application.borrow_mut().find(&command_name)?); + } - fn get_aliases(&self) -> Vec<String> { - self.inner.get_aliases() - } + let helper = DescriptorHelper::new(); + // TODO: DescriptorHelper::describe2 takes the described object as Option<PhpMixed>, + // but PhpMixed cannot hold a Command. The Command/Application object mixing for + // describe needs a dedicated type (Phase C). + let object: Option<PhpMixed> = todo!(); + let mut options = indexmap::IndexMap::new(); + options.insert("format".to_string(), input.borrow().get_option("format")?); + options.insert("raw_text".to_string(), input.borrow().get_option("raw")?); + let _ = helper.describe2(&mut *output.borrow_mut(), object, options); - fn get_synopsis(&mut self, short: bool) -> String { - self.inner.get_synopsis(short) - } + self.command = None; - fn get_usages(&self) -> Vec<String> { - self.inner.get_usages() + Ok(0) } - fn get_helper( - &self, - name: &str, - ) -> anyhow::Result< - Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, - > { - self.inner.get_helper(name) + fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { + self.complete_impl(input, suggestions); } - fn ignore_validation_errors(&mut self) { - self.inner.ignore_validation_errors(); - } + crate::delegate_command_trait_impls_to_inner!(inner); } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs index 33bc5d4..978ac92 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs @@ -6,7 +6,7 @@ use std::ops::{Deref, DerefMut}; use std::rc::Rc; use crate::symfony::console::application::Application; -use crate::symfony::console::command::command::{BaseCommand, Command, SetDefinitionArg}; +use crate::symfony::console::command::command::{Command, CommandData, SetDefinitionArg}; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; use crate::symfony::console::helper::helper_set::HelperSet; @@ -33,13 +33,13 @@ impl std::fmt::Debug for LazyCommandInner { #[derive(Debug)] pub struct LazyCommand { - inner: BaseCommand, + inner: CommandData, command: LazyCommandInner, is_enabled: Option<bool>, } impl Deref for LazyCommand { - type Target = BaseCommand; + type Target = CommandData; fn deref(&self) -> &Self::Target { &self.inner @@ -62,25 +62,24 @@ impl LazyCommand { is_enabled: Option<bool>, ) -> anyhow::Result<Self> { let mut this = Self { - inner: BaseCommand::__construct(None)?, + inner: CommandData::new(None), command: LazyCommandInner::Factory(command_factory), is_enabled, }; - this.inner - .set_name(name)? - .set_aliases(aliases)? - .set_hidden(is_hidden) - .set_description(description); + this.inner.set_name(name)?; + this.inner.set_aliases(aliases)?; + this.inner.set_hidden(is_hidden); + this.inner.set_description(description); Ok(this) } - pub fn ignore_validation_errors(&mut self) { + pub fn ignore_validation_errors_impl(&mut self) { self.get_command().ignore_validation_errors(); } - pub fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { + pub fn set_application_impl(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { // if ($this->command instanceof parent) if let LazyCommandInner::Command(command) = &mut self.command { command.set_application(application.clone()); @@ -90,7 +89,7 @@ impl LazyCommand { self.inner.set_application(application); } - pub fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>) { + pub fn set_helper_set_impl(&mut self, helper_set: Rc<RefCell<HelperSet>>) { // if ($this->command instanceof parent) if let LazyCommandInner::Command(command) = &mut self.command { command.set_helper_set(helper_set.clone()); @@ -100,7 +99,7 @@ impl LazyCommand { self.inner.set_helper_set(helper_set); } - pub fn is_enabled(&mut self) -> bool { + pub fn is_enabled_impl(&mut self) -> bool { // $this->isEnabled ?? $this->getCommand()->isEnabled() match self.is_enabled { Some(is_enabled) => is_enabled, @@ -108,113 +107,58 @@ impl LazyCommand { } } - pub fn run( + pub fn run_impl( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { self.get_command().run(input, output) } - pub fn complete(&mut self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { - self.get_command().complete(input, suggestions); - } - - pub fn set_code( + pub fn complete_impl( &mut self, - code: Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> PhpMixed>, - ) -> &mut Self { - // TODO: Command::set_code() lives on BaseCommand's inherent API and is not part of - // the polymorphic `Command` trait, so it cannot be forwarded through `get_command()` - // (a `&mut Box<dyn Command>`). Resolving this needs `set_code` on the trait (Phase C). - let _ = code; - todo!() + input: &CompletionInput, + suggestions: &mut CompletionSuggestions, + ) { + self.get_command().complete(input, suggestions); } /// @internal - pub fn merge_application_definition(&mut self, merge_args: bool) { + pub fn merge_application_definition_impl(&mut self, merge_args: bool) { self.get_command().merge_application_definition(merge_args); } - pub fn set_definition(&mut self, definition: SetDefinitionArg) -> &mut Self { - // TODO: Command::set_definition() is not part of the polymorphic `Command` trait; - // it cannot be forwarded through `get_command()` (Phase C). - let _ = definition; - todo!() - } - - pub fn get_definition(&mut self) -> &InputDefinition { + pub fn get_definition_impl(&mut self) -> &InputDefinition { self.get_command().get_definition() } - pub fn get_native_definition(&mut self) -> &InputDefinition { + pub fn get_native_definition_impl(&mut self) -> &InputDefinition { self.get_command().get_native_definition() } - pub fn add_argument( - &mut self, - name: &str, - mode: Option<i64>, - description: &str, - default: PhpMixed, - ) -> &mut Self { - // TODO: Command::add_argument() is not part of the polymorphic `Command` trait; - // it cannot be forwarded through `get_command()` (Phase C). - let _ = (name, mode, description, default); - todo!() - } - - pub fn add_option( - &mut self, - name: &str, - shortcut: PhpMixed, - mode: Option<i64>, - description: &str, - default: PhpMixed, - ) -> &mut Self { - // TODO: Command::add_option() is not part of the polymorphic `Command` trait; - // it cannot be forwarded through `get_command()` (Phase C). - let _ = (name, shortcut, mode, description, default); - todo!() - } - - pub fn set_process_title(&mut self, title: &str) -> &mut Self { - // TODO: Command::set_process_title() is not part of the polymorphic `Command` trait; - // it cannot be forwarded through `get_command()` (Phase C). - let _ = title; - todo!() - } - - pub fn set_help(&mut self, help: &str) -> &mut Self { + pub fn set_help_impl(&mut self, help: &str) -> &mut Self { self.get_command().set_help(help); self } - pub fn get_help(&mut self) -> String { + pub fn get_help_impl(&mut self) -> String { self.get_command().get_help() } - pub fn get_processed_help(&mut self) -> String { + pub fn get_processed_help_impl(&mut self) -> String { self.get_command().get_processed_help() } - pub fn get_synopsis(&mut self, short: bool) -> String { + pub fn get_synopsis_impl(&mut self, short: bool) -> String { self.get_command().get_synopsis(short) } - pub fn add_usage(&mut self, usage: &str) -> &mut Self { - // TODO: Command::add_usage() is not part of the polymorphic `Command` trait; - // it cannot be forwarded through `get_command()` (Phase C). - let _ = usage; - todo!() - } - - pub fn get_usages(&mut self) -> Vec<String> { + pub fn get_usages_impl(&mut self) -> Vec<String> { self.get_command().get_usages() } - pub fn get_helper( + pub fn get_helper_impl( &mut self, name: &str, ) -> anyhow::Result< @@ -265,16 +209,17 @@ impl LazyCommand { } impl Command for LazyCommand { - fn configure(&mut self) { + fn configure(&mut self) -> anyhow::Result<()> { // LazyCommand has no configure() of its own; nothing to do. + Ok(()) } fn run( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - LazyCommand::run(self, input, output) + LazyCommand::run_impl(self, input, output) } fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) { @@ -291,7 +236,7 @@ impl Command for LazyCommand { } fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { - LazyCommand::set_application(self, application); + LazyCommand::set_application_impl(self, application); } fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> { @@ -299,7 +244,7 @@ impl Command for LazyCommand { } fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>) { - LazyCommand::set_helper_set(self, helper_set); + LazyCommand::set_helper_set_impl(self, helper_set); } fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { @@ -307,7 +252,7 @@ impl Command for LazyCommand { } fn merge_application_definition(&mut self, merge_args: bool) { - LazyCommand::merge_application_definition(self, merge_args); + LazyCommand::merge_application_definition_impl(self, merge_args); } fn get_definition(&self) -> &InputDefinition { @@ -322,14 +267,24 @@ impl Command for LazyCommand { } fn set_name(&mut self, name: &str) -> anyhow::Result<()> { - self.inner.set_name(name)?; - Ok(()) + self.inner.set_name(name) } fn get_name(&self) -> Option<String> { self.inner.get_name() } + fn set_process_title(&mut self, title: &str) { + // TODO: Command::set_process_title() forwards to the wrapped command, which needs lazy + // materialization (Phase C). + let _ = title; + todo!() + } + + fn get_process_title(&self) -> Option<String> { + self.inner.get_process_title() + } + fn set_hidden(&mut self, hidden: bool) { self.inner.set_hidden(hidden); } @@ -347,7 +302,7 @@ impl Command for LazyCommand { } fn set_help(&mut self, help: &str) { - LazyCommand::set_help(self, help); + LazyCommand::set_help_impl(self, help); } fn get_help(&self) -> String { @@ -362,8 +317,7 @@ impl Command for LazyCommand { } fn set_aliases(&mut self, aliases: Vec<String>) -> anyhow::Result<()> { - self.inner.set_aliases(aliases)?; - Ok(()) + self.inner.set_aliases(aliases) } fn get_aliases(&self) -> Vec<String> { @@ -371,7 +325,13 @@ impl Command for LazyCommand { } fn get_synopsis(&mut self, short: bool) -> String { - LazyCommand::get_synopsis(self, short) + LazyCommand::get_synopsis_impl(self, short) + } + + fn add_usage(&mut self, usage: &str) { + // TODO: Command::add_usage() forwards to the wrapped command (Phase C). + let _ = usage; + todo!() } fn get_usages(&self) -> Vec<String> { @@ -391,7 +351,62 @@ impl Command for LazyCommand { todo!() } + fn set_code( + &mut self, + code: Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> PhpMixed>, + ) { + // TODO: Command::set_code() forwards to the wrapped command (Phase C). + let _ = code; + todo!() + } + + fn get_code( + &self, + ) -> Option<&Box<dyn Fn(&mut dyn InputInterface, &mut dyn OutputInterface) -> PhpMixed>> { + self.inner.get_code() + } + fn ignore_validation_errors(&mut self) { - LazyCommand::ignore_validation_errors(self); + LazyCommand::ignore_validation_errors_impl(self); + } + + fn get_ignore_validation_errors(&self) -> bool { + self.inner.get_ignore_validation_errors() + } +} + +/// `set_definition`/`add_argument`/`add_option` are not part of the polymorphic `Command` +/// trait (they take Composer-typed definition entries via `BaseCommand`), so the lazy proxy +/// exposes the Symfony-typed forms as inherent methods mirroring the PHP overrides. +impl LazyCommand { + pub fn set_definition(&mut self, definition: SetDefinitionArg) -> &mut Self { + // TODO: forwards to the wrapped command, which needs lazy materialization (Phase C). + let _ = definition; + todo!() + } + + pub fn add_argument( + &mut self, + name: &str, + mode: Option<i64>, + description: &str, + default: PhpMixed, + ) -> &mut Self { + // TODO: forwards to the wrapped command, which needs lazy materialization (Phase C). + let _ = (name, mode, description, default); + todo!() + } + + pub fn add_option( + &mut self, + name: &str, + shortcut: PhpMixed, + mode: Option<i64>, + description: &str, + default: PhpMixed, + ) -> &mut Self { + // TODO: forwards to the wrapped command, which needs lazy materialization (Phase C). + let _ = (name, shortcut, mode, description, default); + todo!() } } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs index ffb63ab..9edebe4 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs @@ -1,6 +1,6 @@ //! ref: composer/vendor/symfony/console/Command/ListCommand.php -use crate::symfony::console::command::command::{BaseCommand, Command, SetDefinitionArg}; +use crate::symfony::console::command::command::{Command, CommandData, SetDefinitionArg}; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::{ CompletionSuggestions, StringOrSuggestion, @@ -20,11 +20,11 @@ use std::rc::Rc; /// ListCommand displays the list of all available commands for the application. #[derive(Debug)] pub struct ListCommand { - inner: BaseCommand, + inner: CommandData, } impl Deref for ListCommand { - type Target = BaseCommand; + type Target = CommandData; fn deref(&self) -> &Self::Target { &self.inner @@ -38,78 +38,14 @@ impl DerefMut for ListCommand { } impl ListCommand { - fn configure(&mut self) -> anyhow::Result<()> { - self.inner - .set_name("list")? - .set_definition(SetDefinitionArg::Array(vec![ - DefinitionItem::InputArgument(InputArgument::new( - "namespace".to_string(), - Some(InputArgument::OPTIONAL), - "The namespace name".to_string(), - PhpMixed::Null, - )?), - DefinitionItem::InputOption(InputOption::new( - "raw", - PhpMixed::Null, - Some(InputOption::VALUE_NONE), - "To output raw command list".to_string(), - PhpMixed::Null, - )?), - DefinitionItem::InputOption(InputOption::new( - "format", - PhpMixed::Null, - Some(InputOption::VALUE_REQUIRED), - "The output format (txt, xml, json, or md)".to_string(), - PhpMixed::from("txt".to_string()), - )?), - DefinitionItem::InputOption(InputOption::new( - "short", - PhpMixed::Null, - Some(InputOption::VALUE_NONE), - "To skip describing commands' arguments".to_string(), - PhpMixed::Null, - )?), - ])) - .set_description("List commands") - .set_help( - "The <info>%command.name%</info> command lists all commands:\n\ - \n\ - \x20\x20<info>%command.full_name%</info>\n\ - \n\ - You can also display the commands for a specific namespace:\n\ - \n\ - \x20\x20<info>%command.full_name% test</info>\n\ - \n\ - You can also output the information in other formats by using the <comment>--format</comment> option:\n\ - \n\ - \x20\x20<info>%command.full_name% --format=xml</info>\n\ - \n\ - It's also possible to get raw list of commands (useful for embedding command runner):\n\ - \n\ - \x20\x20<info>%command.full_name% --raw</info>", - ); - - Ok(()) - } - - fn execute( - &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, - ) -> anyhow::Result<i64> { - let helper = DescriptorHelper::new(); - // TODO: DescriptorHelper::describe2 takes the described object as Option<PhpMixed>, - // but PhpMixed cannot hold an Application. The Command/Application object mixing for - // describe needs a dedicated type (Phase C). - let object: Option<PhpMixed> = todo!(); - let mut options = indexmap::IndexMap::new(); - options.insert("format".to_string(), input.get_option("format")?); - options.insert("raw_text".to_string(), input.get_option("raw")?); - options.insert("namespace".to_string(), input.get_argument("namespace")?); - options.insert("short".to_string(), input.get_option("short")?); - let _ = helper.describe2(output, object, options); - - Ok(0) + pub fn new() -> Self { + let mut command = ListCommand { + inner: CommandData::new(None), + }; + command + .configure() + .expect("ListCommand::configure uses static, valid metadata"); + command } pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { @@ -142,130 +78,85 @@ impl ListCommand { } impl Command for ListCommand { - fn configure(&mut self) { - let _ = ListCommand::configure(self); + fn configure(&mut self) -> anyhow::Result<()> { + self.inner.set_name("list")?; + self.inner.set_definition(SetDefinitionArg::Array(vec![ + DefinitionItem::InputArgument(InputArgument::new( + "namespace".to_string(), + Some(InputArgument::OPTIONAL), + "The namespace name".to_string(), + PhpMixed::Null, + )?), + DefinitionItem::InputOption(InputOption::new( + "raw", + PhpMixed::Null, + Some(InputOption::VALUE_NONE), + "To output raw command list".to_string(), + PhpMixed::Null, + )?), + DefinitionItem::InputOption(InputOption::new( + "format", + PhpMixed::Null, + Some(InputOption::VALUE_REQUIRED), + "The output format (txt, xml, json, or md)".to_string(), + PhpMixed::from("txt".to_string()), + )?), + DefinitionItem::InputOption(InputOption::new( + "short", + PhpMixed::Null, + Some(InputOption::VALUE_NONE), + "To skip describing commands' arguments".to_string(), + PhpMixed::Null, + )?), + ])); + self.inner.set_description("List commands"); + self.inner.set_help( + "The <info>%command.name%</info> command lists all commands:\n\ + \n\ + \x20\x20<info>%command.full_name%</info>\n\ + \n\ + You can also display the commands for a specific namespace:\n\ + \n\ + \x20\x20<info>%command.full_name% test</info>\n\ + \n\ + You can also output the information in other formats by using the <comment>--format</comment> option:\n\ + \n\ + \x20\x20<info>%command.full_name% --format=xml</info>\n\ + \n\ + It's also possible to get raw list of commands (useful for embedding command runner):\n\ + \n\ + \x20\x20<info>%command.full_name% --raw</info>", + ); + + Ok(()) } - fn run( + fn execute( &mut self, - input: &mut dyn InputInterface, - output: &mut dyn OutputInterface, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - self.inner.run(input, output) + let helper = DescriptorHelper::new(); + // TODO: DescriptorHelper::describe2 takes the described object as Option<PhpMixed>, + // but PhpMixed cannot hold an Application. The Command/Application object mixing for + // describe needs a dedicated type (Phase C). + let object: Option<PhpMixed> = todo!(); + let mut options = indexmap::IndexMap::new(); + options.insert("format".to_string(), input.borrow().get_option("format")?); + options.insert("raw_text".to_string(), input.borrow().get_option("raw")?); + options.insert( + "namespace".to_string(), + input.borrow().get_argument("namespace")?, + ); + options.insert("short".to_string(), input.borrow().get_option("short")?); + let _ = helper.describe2(&mut *output.borrow_mut(), object, options); + + Ok(0) } fn complete(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { self.complete_impl(input, suggestions); } - fn is_enabled(&self) -> bool { - self.inner.is_enabled() - } - - fn set_application( - &mut self, - application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, - ) { - self.inner.set_application(application); - } - - fn get_application( - &self, - ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { - self.inner.get_application() - } - - fn set_helper_set( - &mut self, - helper_set: Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>, - ) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set( - &self, - ) -> Option<Rc<RefCell<crate::symfony::console::helper::helper_set::HelperSet>>> { - self.inner.get_helper_set() - } - - fn merge_application_definition(&mut self, merge_args: bool) { - self.inner.merge_application_definition(merge_args); - } - - fn get_definition(&self) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_definition() - } - - fn get_native_definition( - &self, - ) -> &crate::symfony::console::input::input_definition::InputDefinition { - self.inner.get_native_definition() - } - - fn set_name(&mut self, name: &str) -> anyhow::Result<()> { - self.inner.set_name(name)?; - Ok(()) - } - - fn get_name(&self) -> Option<String> { - self.inner.get_name() - } - - fn set_hidden(&mut self, hidden: bool) { - self.inner.set_hidden(hidden); - } - - fn is_hidden(&self) -> bool { - self.inner.is_hidden() - } - - fn set_description(&mut self, description: &str) { - self.inner.set_description(description); - } - - fn get_description(&self) -> String { - self.inner.get_description() - } - - fn set_help(&mut self, help: &str) { - self.inner.set_help(help); - } - - fn get_help(&self) -> String { - self.inner.get_help() - } - - fn get_processed_help(&self) -> String { - self.inner.get_processed_help() - } - - fn set_aliases(&mut self, aliases: Vec<String>) -> anyhow::Result<()> { - self.inner.set_aliases(aliases)?; - Ok(()) - } - - fn get_aliases(&self) -> Vec<String> { - self.inner.get_aliases() - } - - fn get_synopsis(&mut self, short: bool) -> String { - self.inner.get_synopsis(short) - } - - fn get_usages(&self) -> Vec<String> { - self.inner.get_usages() - } - - fn get_helper( - &self, - name: &str, - ) -> anyhow::Result< - Result<PhpMixed, crate::symfony::console::exception::logic_exception::LogicException>, - > { - self.inner.get_helper(name) - } - - fn ignore_validation_errors(&mut self) { - self.inner.ignore_validation_errors(); - } + crate::delegate_command_trait_impls_to_inner!(inner); } diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs index bc825d5..4a732bd 100644 --- a/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs +++ b/crates/shirabe-external-packages/src/symfony/console/input/input_argument.rs @@ -4,7 +4,7 @@ use crate::symfony::console::exception::invalid_argument_exception::InvalidArgum use crate::symfony::console::exception::logic_exception::LogicException; use shirabe_php_shim::PhpMixed; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct InputArgument { name: String, mode: i64, diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs index afb36a5..24ad27b 100644 --- a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs +++ b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs @@ -4,7 +4,7 @@ use crate::symfony::console::exception::invalid_argument_exception::InvalidArgum use crate::symfony::console::exception::logic_exception::LogicException; use shirabe_php_shim::PhpMixed; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct InputOption { name: String, shortcut: Option<String>, @@ -168,7 +168,8 @@ impl InputOption { let default = if self.is_array() { match default { PhpMixed::Null => PhpMixed::List(vec![]), - PhpMixed::List(_) => default, + // PHP `is_array()` accepts both list-style and associative arrays. + PhpMixed::List(_) | PhpMixed::Array(_) => default, _ => { return Err(LogicException(shirabe_php_shim::LogicException { message: "A default value for an array option must be an array." |
