From 6051927c8fa32cfffa102d2a170c5a6cf747a1b9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 9 Aug 2026 11:19:03 +0900 Subject: refactor(symfony-console): extract symfony/console into the shirabe-symfony-console crate Move `Symfony\Component\Console` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_console::application::Application` instead of `shirabe_external_packages::symfony::console::application::Application`. The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!` macros move with it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/input/input_option.rs | 193 +++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 crates/shirabe-symfony-console/src/input/input_option.rs (limited to 'crates/shirabe-symfony-console/src/input/input_option.rs') diff --git a/crates/shirabe-symfony-console/src/input/input_option.rs b/crates/shirabe-symfony-console/src/input/input_option.rs new file mode 100644 index 00000000..7312e552 --- /dev/null +++ b/crates/shirabe-symfony-console/src/input/input_option.rs @@ -0,0 +1,193 @@ +//! ref: composer/vendor/symfony/console/Input/InputOption.php + +use crate::exception::invalid_argument_exception::InvalidArgumentException; +use crate::exception::logic_exception::LogicException; +use shirabe_php_shim::{PhpMixed, php_regex}; + +#[derive(Debug, Clone)] +pub struct InputOption { + name: String, + shortcut: Option, + mode: i64, + default: PhpMixed, + description: String, +} + +impl InputOption { + pub const VALUE_NONE: i64 = 1; + pub const VALUE_REQUIRED: i64 = 2; + pub const VALUE_OPTIONAL: i64 = 4; + pub const VALUE_IS_ARRAY: i64 = 8; + pub const VALUE_NEGATABLE: i64 = 16; + + pub fn new( + name: &str, + shortcut: PhpMixed, + mode: Option, + description: String, + default: PhpMixed, + ) -> anyhow::Result { + let name = if let Some(stripped) = name.strip_prefix("--") { + stripped.to_string() + } else { + name.to_string() + }; + + if name.is_empty() { + return Err(InvalidArgumentException::new( + "An option name cannot be empty.".to_string(), + ) + .into()); + } + + let shortcut = match shortcut { + PhpMixed::String(ref s) if s.is_empty() => None, + PhpMixed::List(ref v) if v.is_empty() => None, + PhpMixed::Bool(false) => None, + PhpMixed::Null => None, + PhpMixed::List(ref arr) => { + let parts: Vec = arr + .iter() + .filter_map(|v| { + if let PhpMixed::String(s) = v { + Some(s.clone()) + } else { + None + } + }) + .collect(); + let joined = shirabe_php_shim::implode("|", &parts); + Self::normalize_shortcut(joined)? + } + PhpMixed::String(s) => Self::normalize_shortcut(s)?, + _ => None, + }; + + let mode = match mode { + None => Self::VALUE_NONE, + Some(m) if !(1..(Self::VALUE_NEGATABLE << 1)).contains(&m) => { + return Err(InvalidArgumentException::new(format!( + "Option mode \"{}\" is not valid.", + m + )) + .into()); + } + Some(m) => m, + }; + + let mut option = InputOption { + name, + shortcut, + mode, + description, + default: PhpMixed::Null, + }; + + if option.is_array() && !option.accept_value() { + return Err(InvalidArgumentException::new("Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.".to_string()) + .into()); + } + if option.is_negatable() && option.accept_value() { + return Err(InvalidArgumentException::new("Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.".to_string()) + .into()); + } + + option.set_default(default)?; + + Ok(option) + } + + fn normalize_shortcut(s: String) -> anyhow::Result> { + let stripped = shirabe_php_shim::ltrim(&s, Some("-")); + let parts = shirabe_php_shim::preg_split(php_regex!(r"{(\|)-?}"), &stripped); + let filtered: Vec = + shirabe_php_shim::array_filter(&parts, |s: &String| !s.is_empty()); + let result = shirabe_php_shim::implode("|", &filtered); + if result.is_empty() { + return Err(InvalidArgumentException::new( + "An option shortcut cannot be empty.".to_string(), + ) + .into()); + } + Ok(Some(result)) + } + + pub fn get_shortcut(&self) -> Option<&str> { + self.shortcut.as_deref() + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn accept_value(&self) -> bool { + self.is_value_required() || self.is_value_optional() + } + + pub fn is_value_required(&self) -> bool { + Self::VALUE_REQUIRED == (Self::VALUE_REQUIRED & self.mode) + } + + pub fn is_value_optional(&self) -> bool { + Self::VALUE_OPTIONAL == (Self::VALUE_OPTIONAL & self.mode) + } + + pub fn is_array(&self) -> bool { + Self::VALUE_IS_ARRAY == (Self::VALUE_IS_ARRAY & self.mode) + } + + pub fn is_negatable(&self) -> bool { + Self::VALUE_NEGATABLE == (Self::VALUE_NEGATABLE & self.mode) + } + + pub fn set_default(&mut self, default: PhpMixed) -> anyhow::Result<()> { + if Self::VALUE_NONE == (Self::VALUE_NONE & self.mode) && !matches!(default, PhpMixed::Null) + { + return Err(LogicException::new( + "Cannot set a default value when using InputOption::VALUE_NONE mode.".to_string(), + ) + .into()); + } + + let default = if self.is_array() { + match default { + PhpMixed::Null => PhpMixed::List(vec![]), + // PHP `is_array()` accepts both list-style and associative arrays. + PhpMixed::List(_) | PhpMixed::Array(_) => default, + _ => { + return Err(LogicException::new( + "A default value for an array option must be an array.".to_string(), + ) + .into()); + } + } + } else { + default + }; + + self.default = if self.accept_value() || self.is_negatable() { + default + } else { + PhpMixed::Bool(false) + }; + Ok(()) + } + + pub fn get_default(&self) -> &PhpMixed { + &self.default + } + + pub fn get_description(&self) -> &str { + &self.description + } + + pub fn equals(&self, option: &InputOption) -> bool { + option.get_name() == self.get_name() + && option.get_shortcut() == self.get_shortcut() + && option.get_default() == self.get_default() + && option.is_negatable() == self.is_negatable() + && option.is_array() == self.is_array() + && option.is_value_required() == self.is_value_required() + && option.is_value_optional() == self.is_value_optional() + } +} -- cgit v1.3.1-4-g156e