From 0800c90a7ec0bc7a3d4c13063dfe6d2298a557bc Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 19 Aug 2026 23:45:37 +0900 Subject: fix(input): thread a typed InputValue through the input layer Options and arguments were stored and passed as PhpMixed even though Symfony only ever puts a string, a bool, a list of strings or null in one. get_option already narrowed to InputOptionValue at the boundary; this widens that enum into InputValue and pushes it through InputInterface, InputOption/InputArgument defaults, the Input storage, ArgvInput/ArrayInput/StringInput/CompletionInput, Command::add_option and add_argument, and the Composer-side wrappers. Two neighbouring string|int unions get types of their own: InputDefinition::{get_argument,has_argument} take an ArgumentName, and ArrayInput keys its parameters by ParameterName. has_parameter_option and get_parameter_option take the values they look for as &[&str], which is what PHP's `(array) $values` cast produced anyway. Two behaviours change along the way. Input::set_option on a negated option now negates with PHP's loose bool cast rather than treating a non-bool as false, matching `!$value`. ArrayInput::parse now resolves an integer key to an argument position instead of looking up an argument literally named "0". Co-Authored-By: Claude Opus 5 (1M context) --- .../shirabe-symfony-console/src/command/command.rs | 13 +- .../src/command/complete_command.rs | 17 ++- .../src/command/dump_completion_command.rs | 7 +- .../src/command/help_command.rs | 15 +- .../src/command/list_command.rs | 19 +-- .../src/completion/completion_input.rs | 34 ++--- .../src/descriptor/json_descriptor.rs | 23 +-- .../src/descriptor/markdown_descriptor.rs | 4 +- .../src/descriptor/text_descriptor.rs | 20 +-- .../src/descriptor/xml_descriptor.rs | 18 ++- crates/shirabe-symfony-console/src/input.rs | 2 + .../src/input/argv_input.rs | 153 ++++++++++--------- .../src/input/array_input.rs | 167 +++++++++++---------- crates/shirabe-symfony-console/src/input/input.rs | 47 +++--- .../src/input/input_argument.rs | 18 +-- .../src/input/input_definition.rs | 53 ++++--- .../src/input/input_interface.rs | 23 ++- .../src/input/input_option.rs | 138 ++--------------- .../src/input/input_value.rs | 146 ++++++++++++++++++ .../src/input/string_input.rs | 24 +-- 20 files changed, 496 insertions(+), 445 deletions(-) create mode 100644 crates/shirabe-symfony-console/src/input/input_value.rs (limited to 'crates/shirabe-symfony-console') diff --git a/crates/shirabe-symfony-console/src/command/command.rs b/crates/shirabe-symfony-console/src/command/command.rs index 27966ec1..3fd39bc4 100644 --- a/crates/shirabe-symfony-console/src/command/command.rs +++ b/crates/shirabe-symfony-console/src/command/command.rs @@ -9,6 +9,7 @@ use crate::input::InputArgument; use crate::input::InputDefinition; use crate::input::InputInterface; use crate::input::InputOption; +use crate::input::InputValue; use crate::output::OutputInterface; use indexmap::IndexMap; use shirabe_php_shim::{PhpMixed, php_regex, preg_is_match}; @@ -148,7 +149,7 @@ impl CommandData { name: &str, mode: Option, description: &str, - default: PhpMixed, + default: InputValue, ) -> anyhow::Result<&Self> { self.definition .borrow_mut() @@ -178,10 +179,10 @@ impl CommandData { pub fn add_option( &self, name: &str, - shortcut: PhpMixed, + shortcut: Option<&str>, mode: Option, description: &str, - default: PhpMixed, + default: InputValue, ) -> anyhow::Result<&Self> { self.definition .borrow_mut() @@ -189,7 +190,7 @@ impl CommandData { .unwrap() .add_option(InputOption::new( name, - shortcut.clone(), + shortcut, mode, description.to_string(), default.clone(), @@ -403,12 +404,12 @@ pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny + shirabe_php_shim: // 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.borrow().has_argument("command") - && matches!(input.borrow().get_argument("command")?, PhpMixed::Null) + && input.borrow().get_argument("command")?.is_null() { let name = self.get_name(); input .borrow_mut() - .set_argument("command", PhpMixed::from(name))?; + .set_argument("command", InputValue::from(name))?; } input.borrow_mut().validate()?; diff --git a/crates/shirabe-symfony-console/src/command/complete_command.rs b/crates/shirabe-symfony-console/src/command/complete_command.rs index 432888b6..2f540f42 100644 --- a/crates/shirabe-symfony-console/src/command/complete_command.rs +++ b/crates/shirabe-symfony-console/src/command/complete_command.rs @@ -7,6 +7,7 @@ use crate::completion::CompletionOutputInterface; use crate::completion::{CompletionSuggestions, StringOrSuggestion}; use crate::input::InputInterface; use crate::input::InputOption; +use crate::input::InputValue; use crate::output::OutputInterface; use indexmap::IndexMap; use shirabe_php_shim::{PhpMixed, impl_php_class}; @@ -191,31 +192,31 @@ impl Command for CompleteCommand { self.inner .add_option( "shell", - PhpMixed::from("s".to_string()), + Some("s"), Some(InputOption::VALUE_REQUIRED), &format!("The shell type (\"{}\")", shells), - PhpMixed::Null, + InputValue::Null, )? .add_option( "input", - PhpMixed::from("i".to_string()), + Some("i"), Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "An array of input tokens (e.g. COMP_WORDS or argv)", - PhpMixed::Null, + InputValue::Null, )? .add_option( "current", - PhpMixed::from("c".to_string()), + Some("c"), Some(InputOption::VALUE_REQUIRED), "The index of the \"input\" array that the cursor is in (e.g. COMP_CWORD)", - PhpMixed::Null, + InputValue::Null, )? .add_option( "symfony", - PhpMixed::from("S".to_string()), + Some("S"), Some(InputOption::VALUE_REQUIRED), "The version of the completion script", - PhpMixed::Null, + InputValue::Null, )?; Ok(()) diff --git a/crates/shirabe-symfony-console/src/command/dump_completion_command.rs b/crates/shirabe-symfony-console/src/command/dump_completion_command.rs index 4db6c233..49a66f6d 100644 --- a/crates/shirabe-symfony-console/src/command/dump_completion_command.rs +++ b/crates/shirabe-symfony-console/src/command/dump_completion_command.rs @@ -6,6 +6,7 @@ use crate::completion::{CompletionSuggestions, StringOrSuggestion}; use crate::input::InputArgument; use crate::input::InputInterface; use crate::input::InputOption; +use crate::input::InputValue; use crate::output::OutputInterface; use crate::output::output_interface; use shirabe_php_shim::{PhpMixed, impl_php_class}; @@ -189,14 +190,14 @@ impl Command for DumpCompletionCommand { "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, + InputValue::Null, )?; self.inner.add_option( "debug", - PhpMixed::Null, + None, Some(InputOption::VALUE_NONE), "Tail the completion debug log", - PhpMixed::Null, + InputValue::Null, )?; Ok(()) diff --git a/crates/shirabe-symfony-console/src/command/help_command.rs b/crates/shirabe-symfony-console/src/command/help_command.rs index 2081e0e1..4d8f7dbd 100644 --- a/crates/shirabe-symfony-console/src/command/help_command.rs +++ b/crates/shirabe-symfony-console/src/command/help_command.rs @@ -10,8 +10,9 @@ use crate::input::DefinitionItem; use crate::input::InputArgument; use crate::input::InputInterface; use crate::input::InputOption; +use crate::input::InputValue; use crate::output::OutputInterface; -use shirabe_php_shim::{PhpMixed, impl_php_class}; +use shirabe_php_shim::impl_php_class; use std::ops::{Deref, DerefMut}; /// HelpCommand displays the help for a given command. @@ -101,21 +102,21 @@ impl Command for HelpCommand { "command_name".to_string(), Some(InputArgument::OPTIONAL), "The command name".to_string(), - PhpMixed::from("help".to_string()), + InputValue::from("help".to_string()), )?), DefinitionItem::InputOption(InputOption::new( "format", - PhpMixed::Null, + None, Some(InputOption::VALUE_REQUIRED), "The output format (txt, xml, json, or md)".to_string(), - PhpMixed::from("txt".to_string()), + InputValue::from("txt".to_string()), )?), DefinitionItem::InputOption(InputOption::new( "raw", - PhpMixed::Null, + None, Some(InputOption::VALUE_NONE), "To output raw command help".to_string(), - PhpMixed::Null, + InputValue::Null, )?), ])); self.inner.set_description("Display help for a command"); @@ -141,7 +142,7 @@ impl Command for HelpCommand { ) -> anyhow::Result { if self.command.borrow().is_none() { let application = self.get_application().unwrap(); - let command_name = input.borrow().get_argument("command_name")?.to_string(); + let command_name = input.borrow().get_argument("command_name")?.to_php_string(); let found = application.borrow_mut().find(&command_name)?; *self.command.borrow_mut() = Some(found); } diff --git a/crates/shirabe-symfony-console/src/command/list_command.rs b/crates/shirabe-symfony-console/src/command/list_command.rs index 6074b49a..08faea71 100644 --- a/crates/shirabe-symfony-console/src/command/list_command.rs +++ b/crates/shirabe-symfony-console/src/command/list_command.rs @@ -10,8 +10,9 @@ use crate::input::DefinitionItem; use crate::input::InputArgument; use crate::input::InputInterface; use crate::input::InputOption; +use crate::input::InputValue; use crate::output::OutputInterface; -use shirabe_php_shim::{PhpMixed, impl_php_class}; +use shirabe_php_shim::impl_php_class; use std::ops::{Deref, DerefMut}; /// ListCommand displays the list of all available commands for the application. @@ -93,28 +94,28 @@ impl Command for ListCommand { "namespace".to_string(), Some(InputArgument::OPTIONAL), "The namespace name".to_string(), - PhpMixed::Null, + InputValue::Null, )?), DefinitionItem::InputOption(InputOption::new( "raw", - PhpMixed::Null, + None, Some(InputOption::VALUE_NONE), "To output raw command list".to_string(), - PhpMixed::Null, + InputValue::Null, )?), DefinitionItem::InputOption(InputOption::new( "format", - PhpMixed::Null, + None, Some(InputOption::VALUE_REQUIRED), "The output format (txt, xml, json, or md)".to_string(), - PhpMixed::from("txt".to_string()), + InputValue::from("txt".to_string()), )?), DefinitionItem::InputOption(InputOption::new( "short", - PhpMixed::Null, + None, Some(InputOption::VALUE_NONE), "To skip describing commands' arguments".to_string(), - PhpMixed::Null, + InputValue::Null, )?), ])); self.inner.set_description("List commands"); @@ -157,7 +158,7 @@ impl Command for ListCommand { ); options.insert( "namespace".to_string(), - input.borrow().get_argument("namespace")?, + input.borrow().get_argument("namespace")?.into(), ); options.insert( "short".to_string(), diff --git a/crates/shirabe-symfony-console/src/completion/completion_input.rs b/crates/shirabe-symfony-console/src/completion/completion_input.rs index c667ef60..419e6e80 100644 --- a/crates/shirabe-symfony-console/src/completion/completion_input.rs +++ b/crates/shirabe-symfony-console/src/completion/completion_input.rs @@ -3,7 +3,8 @@ use crate::input::ArgvInput; use crate::input::InputDefinition; use crate::input::InputOption; -use shirabe_php_shim::{PhpMixed, php_regex, preg_match_all}; +use crate::input::InputValue; +use shirabe_php_shim::{php_regex, preg_match_all}; /// An input specialized for shell completion. /// @@ -148,13 +149,10 @@ impl CompletionInput { let argument_value = self.inner.inner.arguments[¤t_argument_name].clone(); self.completion_name = Some(current_argument_name.clone()); - if let PhpMixed::List(argument_value) = &argument_value { - self.completion_value = argument_value - .last() - .map(|v| v.to_string()) - .unwrap_or_default(); + if let InputValue::Array(argument_value) = &argument_value { + self.completion_value = argument_value.last().cloned().unwrap_or_default(); } else { - self.completion_value = argument_value.to_string(); + self.completion_value = argument_value.to_php_string(); } } @@ -165,7 +163,7 @@ impl CompletionInput { .inner .inner .definition - .get_argument(&PhpMixed::String(argument_name.clone())) + .get_argument(&crate::input::ArgumentName::of(&argument_name)) .unwrap() .is_array() { @@ -304,16 +302,16 @@ impl crate::input::InputInterface for CompletionInput { self.to_string() } - fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { + fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { crate::input::InputInterface::has_parameter_option(&self.inner, values, only_params) } fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { + ) -> InputValue { crate::input::InputInterface::get_parameter_option( &self.inner, values, @@ -330,15 +328,15 @@ impl crate::input::InputInterface for CompletionInput { crate::input::InputInterface::validate(&mut self.inner) } - fn get_arguments(&self) -> indexmap::IndexMap { + fn get_arguments(&self) -> indexmap::IndexMap { crate::input::InputInterface::get_arguments(&self.inner) } - fn get_argument(&self, name: &str) -> anyhow::Result { + fn get_argument(&self, name: &str) -> anyhow::Result { crate::input::InputInterface::get_argument(&self.inner, name) } - fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { crate::input::InputInterface::set_argument(&mut self.inner, name, value) } @@ -346,15 +344,15 @@ impl crate::input::InputInterface for CompletionInput { crate::input::InputInterface::has_argument(&self.inner, name) } - fn get_options(&self) -> indexmap::IndexMap { + fn get_options(&self) -> indexmap::IndexMap { crate::input::InputInterface::get_options(&self.inner) } - fn get_option(&self, name: &str) -> anyhow::Result { + fn get_option(&self, name: &str) -> anyhow::Result { crate::input::InputInterface::get_option(&self.inner, name) } - fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { crate::input::InputInterface::set_option(&mut self.inner, name, value) } diff --git a/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs index 53941663..97c1c4f1 100644 --- a/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs +++ b/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs @@ -165,14 +165,9 @@ impl JsonDescriptor { argument.get_description(), )), ); - data.insert( - "default".to_string(), - if matches!(argument.get_default(), PhpMixed::Float(f) if f.is_infinite() && *f > 0.0) { - PhpMixed::String("INF".to_string()) - } else { - argument.get_default().clone() - }, - ); + // TODO(type-model): PHP compares the default against `INF` and serializes it as the + // string "INF"; `InputValue` has no float variant to hold one. + data.insert("default".to_string(), argument.get_default().clone().into()); Ok(data) } @@ -226,15 +221,9 @@ impl JsonDescriptor { option.get_description(), )), ); - data.insert( - "default".to_string(), - if matches!(option.get_default(), PhpMixed::Float(f) if f.is_infinite() && *f > 0.0) - { - PhpMixed::String("INF".to_string()) - } else { - option.get_default().clone() - }, - ); + // TODO(type-model): PHP compares the default against `INF` and serializes it as the + // string "INF"; `InputValue` has no float variant to hold one. + data.insert("default".to_string(), option.get_default().clone().into()); } Ok(data) } diff --git a/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs index d6285468..f7b3cd71 100644 --- a/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs +++ b/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs @@ -64,7 +64,7 @@ impl MarkdownDescriptor { shirabe_php_shim::str_replace( "\n", "", - &shirabe_php_shim::var_export(argument.get_default(), true), + &shirabe_php_shim::var_export(&argument.get_default().to_php_mixed(), true), ), ), true, @@ -104,7 +104,7 @@ impl MarkdownDescriptor { shirabe_php_shim::str_replace( "\n", "", - &shirabe_php_shim::var_export(option.get_default(), true), + &shirabe_php_shim::var_export(&option.get_default().to_php_mixed(), true), ), ), true, diff --git a/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs index 8783ef5e..c15cdebb 100644 --- a/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs +++ b/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs @@ -36,15 +36,14 @@ impl TextDescriptor { argument: &InputArgument, options: IndexMap, ) -> anyhow::Result<()> { - let default = if !argument.get_default().is_null() - && (!matches!( - argument.get_default(), - PhpMixed::List(_) | PhpMixed::Array(_) - ) || shirabe_php_shim::count(argument.get_default()) != 0) + let default_value = argument.get_default().to_php_mixed(); + let default = if !default_value.is_null() + && (!matches!(default_value, PhpMixed::List(_) | PhpMixed::Array(_)) + || shirabe_php_shim::count(&default_value) != 0) { format!( " [default: {}]", - self.format_default_value(argument.get_default())? + self.format_default_value(&default_value)? ) } else { String::new() @@ -82,14 +81,15 @@ impl TextDescriptor { option: &InputOption, options: IndexMap, ) -> anyhow::Result<()> { + let default_value = option.get_default().to_php_mixed(); let default = if option.accept_value() - && !option.get_default().is_null() - && (!matches!(option.get_default(), PhpMixed::List(_) | PhpMixed::Array(_)) - || shirabe_php_shim::count(option.get_default()) != 0) + && !default_value.is_null() + && (!matches!(default_value, PhpMixed::List(_) | PhpMixed::Array(_)) + || shirabe_php_shim::count(&default_value) != 0) { format!( " [default: {}]", - self.format_default_value(option.get_default())? + self.format_default_value(&default_value)? ) } else { String::new() diff --git a/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs index 1a001b22..0b74fe3a 100644 --- a/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs +++ b/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs @@ -233,13 +233,14 @@ impl XmlDescriptor { description_xml.append_child(dom.create_text_node(argument.get_description())); let defaults_xml = object_xml.append_child(dom.create_element("defaults")); - let defaults: Vec = match argument.get_default() { + let default_value = argument.get_default().to_php_mixed(); + let defaults: Vec = match &default_value { PhpMixed::List(_) | PhpMixed::Array(_) => { - self.default_values_as_strings(argument.get_default()) + self.default_values_as_strings(&default_value) } - PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(argument.get_default(), true)], + PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(&default_value, true)], d if shirabe_php_shim::php_truthy(d) => { - vec![shirabe_php_shim::php_to_string(argument.get_default())] + vec![shirabe_php_shim::php_to_string(&default_value)] } _ => vec![], }; @@ -294,13 +295,14 @@ impl XmlDescriptor { description_xml.append_child(dom.create_text_node(option.get_description())); if option.accept_value() { - let defaults: Vec = match option.get_default() { + let default_value = option.get_default().to_php_mixed(); + let defaults: Vec = match &default_value { PhpMixed::List(_) | PhpMixed::Array(_) => { - self.default_values_as_strings(option.get_default()) + self.default_values_as_strings(&default_value) } - PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(option.get_default(), true)], + PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(&default_value, true)], d if shirabe_php_shim::php_truthy(d) => { - vec![shirabe_php_shim::php_to_string(option.get_default())] + vec![shirabe_php_shim::php_to_string(&default_value)] } _ => vec![], }; diff --git a/crates/shirabe-symfony-console/src/input.rs b/crates/shirabe-symfony-console/src/input.rs index f05188fe..c09e590f 100644 --- a/crates/shirabe-symfony-console/src/input.rs +++ b/crates/shirabe-symfony-console/src/input.rs @@ -6,6 +6,7 @@ mod input_aware_interface; mod input_definition; mod input_interface; mod input_option; +mod input_value; mod streamable_input_interface; mod string_input; @@ -17,5 +18,6 @@ pub use input_aware_interface::*; pub use input_definition::*; pub use input_interface::*; pub use input_option::*; +pub use input_value::*; pub use streamable_input_interface::*; pub use string_input::*; diff --git a/crates/shirabe-symfony-console/src/input/argv_input.rs b/crates/shirabe-symfony-console/src/input/argv_input.rs index 6138c195..a0f1d4d3 100644 --- a/crates/shirabe-symfony-console/src/input/argv_input.rs +++ b/crates/shirabe-symfony-console/src/input/argv_input.rs @@ -1,13 +1,14 @@ //! ref: composer/vendor/symfony/console/Input/ArgvInput.php use crate::exception::RuntimeException; +use crate::input::ArgumentName; use crate::input::Input; use crate::input::InputDefinition; use crate::input::InputInterface; -use crate::input::InputOptionValue; +use crate::input::InputValue; use crate::input::StreamableInputInterface; use indexmap::IndexMap; -use shirabe_php_shim::{PhpMixed, php_regex, preg_match}; +use shirabe_php_shim::{php_regex, preg_match}; /// ArgvInput represents an input coming from the CLI arguments. /// @@ -144,13 +145,13 @@ impl ArgvInput { // an option with a value (with no space) self.add_short_option( &first, - PhpMixed::String(shirabe_php_shim::substr(&name, 1, None)), + InputValue::String(shirabe_php_shim::substr(&name, 1, None)), )?; } else { self.parse_short_option_set(&name)?; } } else { - self.add_short_option(&name, PhpMixed::Null)?; + self.add_short_option(&name, InputValue::Null)?; } Ok(()) @@ -180,15 +181,15 @@ impl ArgvInput { let option = self.inner.definition.get_option_for_shortcut(&name_i)?; if option.accept_value() { let value = if i == len - 1 { - PhpMixed::Null + InputValue::Null } else { - PhpMixed::String(shirabe_php_shim::substr(name, i + 1, None)) + InputValue::String(shirabe_php_shim::substr(name, i + 1, None)) }; self.add_long_option(option.get_name(), value)?; break; } else { - self.add_long_option(option.get_name(), PhpMixed::Null)?; + self.add_long_option(option.get_name(), InputValue::Null)?; } i += 1; } @@ -209,11 +210,11 @@ impl ArgvInput { } self.add_long_option( &shirabe_php_shim::substr(&name, 0, Some(pos)), - PhpMixed::String(value), + InputValue::String(value), )?; } None => { - self.add_long_option(&name, PhpMixed::Null)?; + self.add_long_option(&name, InputValue::Null)?; } } @@ -225,34 +226,47 @@ impl ArgvInput { let c = self.inner.arguments.len() as i64; // if input is expecting another argument, add it - if self.inner.definition.has_argument(&PhpMixed::Int(c)) { - let arg = self.inner.definition.get_argument(&PhpMixed::Int(c))?; + if self + .inner + .definition + .has_argument(&ArgumentName::Position(c)) + { + let arg = self + .inner + .definition + .get_argument(&ArgumentName::Position(c))?; let value = if arg.is_array() { - PhpMixed::List(vec![PhpMixed::String(token.to_string())]) + InputValue::Array(vec![token.to_string()]) } else { - PhpMixed::String(token.to_string()) + InputValue::String(token.to_string()) }; self.inner .arguments .insert(arg.get_name().to_string(), value); // if last argument isArray(), append token to last argument - } else if self.inner.definition.has_argument(&PhpMixed::Int(c - 1)) + } else if self + .inner + .definition + .has_argument(&ArgumentName::Position(c - 1)) && self .inner .definition - .get_argument(&PhpMixed::Int(c - 1))? + .get_argument(&ArgumentName::Position(c - 1))? .is_array() { - let arg = self.inner.definition.get_argument(&PhpMixed::Int(c - 1))?; - if let Some(PhpMixed::List(list)) = self.inner.arguments.get_mut(arg.get_name()) { - list.push(PhpMixed::String(token.to_string())); + let arg = self + .inner + .definition + .get_argument(&ArgumentName::Position(c - 1))?; + if let Some(InputValue::Array(list)) = self.inner.arguments.get_mut(arg.get_name()) { + list.push(token.to_string()); } // unexpected argument } else { let mut all = self.inner.definition.get_arguments().clone(); - let mut symfony_command_name: Option = None; + let mut symfony_command_name: Option = None; let first_key = all.keys().next().cloned(); if let Some(key) = &first_key { let input_argument = &all[key]; @@ -265,12 +279,10 @@ impl ArgvInput { let message = if !all.is_empty() { let names: Vec = all.keys().cloned().collect(); match &symfony_command_name { - Some(symfony_command_name) - if !matches!(symfony_command_name, PhpMixed::Null) => - { + Some(symfony_command_name) if !symfony_command_name.is_null() => { format!( "Too many arguments to \"{}\" command, expected arguments \"{}\".", - symfony_command_name.clone(), + symfony_command_name.to_php_string(), shirabe_php_shim::implode("\" \"", &names), ) } @@ -281,12 +293,12 @@ impl ArgvInput { } } else if symfony_command_name .as_ref() - .map(|n| !matches!(n, PhpMixed::Null)) + .map(|n| !n.is_null()) .unwrap_or(false) { format!( "No arguments expected for \"{}\" command, got \"{}\".", - symfony_command_name.unwrap(), + symfony_command_name.unwrap().to_php_string(), token, ) } else { @@ -300,7 +312,7 @@ impl ArgvInput { } /// Adds a short option value. - fn add_short_option(&mut self, shortcut: &str, value: PhpMixed) -> anyhow::Result<()> { + fn add_short_option(&mut self, shortcut: &str, value: InputValue) -> anyhow::Result<()> { if !self.inner.definition.has_shortcut(shortcut) { return Err(RuntimeException::new(format!( "The \"-{}\" option does not exist.", @@ -319,7 +331,7 @@ impl ArgvInput { } /// Adds a long option value. - fn add_long_option(&mut self, name: &str, mut value: PhpMixed) -> anyhow::Result<()> { + fn add_long_option(&mut self, name: &str, mut value: InputValue) -> anyhow::Result<()> { if !self.inner.definition.has_option(name) { if !self.inner.definition.has_negation(name) { return Err(RuntimeException::new(format!( @@ -330,7 +342,7 @@ impl ArgvInput { } let option_name = self.inner.definition.negation_to_name(name)?; - if !matches!(value, PhpMixed::Null) { + if !value.is_null() { return Err(RuntimeException::new(format!( "The \"--{}\" option does not accept a value.", name @@ -339,14 +351,14 @@ impl ArgvInput { } self.inner .options - .insert(option_name, PhpMixed::Bool(false)); + .insert(option_name, InputValue::Bool(false)); return Ok(()); } let option = self.inner.definition.get_option(name)?; - if !matches!(value, PhpMixed::Null) && !option.accept_value() { + if !value.is_null() && !option.accept_value() { return Err(RuntimeException::new(format!( "The \"--{}\" option does not accept a value.", name @@ -355,8 +367,8 @@ impl ArgvInput { } // in_array($value, ['', null], true) - let value_is_empty_or_null = matches!(&value, PhpMixed::String(s) if s.is_empty()) - || matches!(value, PhpMixed::Null); + let value_is_empty_or_null = + matches!(&value, InputValue::String(s) if s.is_empty()) || value.is_null(); if value_is_empty_or_null && option.accept_value() && !self.parsed.is_empty() { // if option accepts an optional or mandatory argument // let's see if there is one provided @@ -364,13 +376,13 @@ impl ArgvInput { // (isset($next[0]) && '-' !== $next[0]) || in_array($next, ['', null], true) let next_first = next.as_bytes().first().copied(); if (next_first.is_some() && next_first != Some(b'-')) || next.is_empty() { - value = PhpMixed::String(next); + value = InputValue::String(next); } else { self.parsed.insert(0, next); } } - if matches!(value, PhpMixed::Null) { + if value.is_null() { if option.is_value_required() { return Err(RuntimeException::new(format!( "The \"--{}\" option requires a value.", @@ -380,19 +392,25 @@ impl ArgvInput { } if !option.is_array() && !option.is_value_optional() { - value = PhpMixed::Bool(true); + value = InputValue::Bool(true); } } if option.is_array() { + // TODO(type-model): PHP appends the value as it stands, so a + // VALUE_OPTIONAL|VALUE_IS_ARRAY option given no value collects a null; + // `InputValue::Array` only holds strings. + let InputValue::String(value) = value else { + panic!("an array option cannot hold {:?}", value) + }; match self.inner.options.get_mut(name) { - Some(PhpMixed::List(list)) => { + Some(InputValue::Array(list)) => { list.push(value); } _ => { self.inner .options - .insert(name.to_string(), PhpMixed::List(vec![value])); + .insert(name.to_string(), InputValue::Array(vec![value])); } } } else { @@ -448,21 +466,19 @@ impl ArgvInput { None } - pub fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { - let values = to_array(values); - + pub fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { for token in &self.tokens { if only_params && token == "--" { return false; } - for value in &values { + for value in values { // Options with values: // For long options, test for '--option=' at beginning // For short options, test for '-o' at beginning let leading = if value.starts_with("--") { format!("{}=", value) } else { - value.clone() + value.to_string() }; if token == value || (!leading.is_empty() && token.starts_with(&leading)) { return true; @@ -475,11 +491,10 @@ impl ArgvInput { pub fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { - let values = to_array(values); + ) -> InputValue { let mut tokens = self.tokens.clone(); while !tokens.is_empty() { @@ -488,11 +503,11 @@ impl ArgvInput { return default; } - for value in &values { + for value in values { if &token == value { return match tokens.first() { - Some(_) => PhpMixed::String(tokens.remove(0)), - None => PhpMixed::Null, + Some(_) => InputValue::String(tokens.remove(0)), + None => InputValue::Null, }; } // Options with values: @@ -501,10 +516,10 @@ impl ArgvInput { let leading = if value.starts_with("--") { format!("{}=", value) } else { - value.clone() + value.to_string() }; if !leading.is_empty() && token.starts_with(&leading) { - return PhpMixed::String(shirabe_php_shim::substr( + return InputValue::String(shirabe_php_shim::substr( &token, shirabe_php_shim::strlen(&leading), None, @@ -553,16 +568,16 @@ impl InputInterface for ArgvInput { ArgvInput::get_first_argument(self) } - fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { + fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { ArgvInput::has_parameter_option(self, values, only_params) } fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { + ) -> InputValue { ArgvInput::get_parameter_option(self, values, default, only_params) } @@ -574,15 +589,15 @@ impl InputInterface for ArgvInput { self.inner.validate() } - fn get_arguments(&self) -> IndexMap { + fn get_arguments(&self) -> IndexMap { self.inner.get_arguments() } - fn get_argument(&self, name: &str) -> anyhow::Result { + fn get_argument(&self, name: &str) -> anyhow::Result { self.inner.get_argument(name) } - fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_argument(name, value) } @@ -590,15 +605,15 @@ impl InputInterface for ArgvInput { self.inner.has_argument(name) } - fn get_options(&self) -> IndexMap { + fn get_options(&self) -> IndexMap { self.inner.get_options() } - fn get_option(&self, name: &str) -> anyhow::Result { + fn get_option(&self, name: &str) -> anyhow::Result { self.inner.get_option(name) } - fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_option(name, value) } @@ -636,19 +651,3 @@ impl StreamableInputInterface for ArgvInput { self.inner.get_stream() } } - -/// PHP `(array) $values` cast: a string becomes a single-element array. -fn to_array(values: PhpMixed) -> Vec { - match values { - PhpMixed::List(list) => list - .into_iter() - .map(|v| shirabe_php_shim::php_to_string(&v)) - .collect(), - PhpMixed::Array(array) => array - .into_iter() - .map(|(_, v)| shirabe_php_shim::php_to_string(&v)) - .collect(), - PhpMixed::Null => vec![], - other => vec![shirabe_php_shim::php_to_string(&other)], - } -} diff --git a/crates/shirabe-symfony-console/src/input/array_input.rs b/crates/shirabe-symfony-console/src/input/array_input.rs index c9bc9dce..437bdde2 100644 --- a/crates/shirabe-symfony-console/src/input/array_input.rs +++ b/crates/shirabe-symfony-console/src/input/array_input.rs @@ -2,13 +2,13 @@ use crate::exception::InvalidArgumentException; use crate::exception::InvalidOptionException; +use crate::input::ArgumentName; use crate::input::Input; use crate::input::InputDefinition; use crate::input::InputInterface; -use crate::input::InputOptionValue; +use crate::input::InputValue; use crate::input::StreamableInputInterface; use indexmap::IndexMap; -use shirabe_php_shim::PhpMixed; /// ArrayInput represents an input provided as an array. /// @@ -19,16 +19,16 @@ use shirabe_php_shim::PhpMixed; /// ``` /// /// PHP arrays can mix integer and string keys; `parameters` preserves both the -/// key type (`PhpMixed::Int` / `PhpMixed::String`) and the insertion order. +/// key type and the insertion order. #[derive(Debug, Clone)] pub struct ArrayInput { inner: Input, - parameters: Vec<(PhpMixed, PhpMixed)>, + parameters: Vec<(ParameterName, InputValue)>, } impl ArrayInput { pub fn new( - parameters: Vec<(PhpMixed, PhpMixed)>, + parameters: Vec<(ParameterName, InputValue)>, definition: Option, ) -> anyhow::Result { let mut array_input = ArrayInput { @@ -58,10 +58,10 @@ impl ArrayInput { Ok(()) } - pub fn get_first_argument(&self) -> Option { + pub fn get_first_argument(&self) -> Option { for (param, value) in &self.parameters { // $param && \is_string($param) && '-' === $param[0] - if let PhpMixed::String(param) = param + if let ParameterName::Name(param) = param && !param.is_empty() && param.as_bytes()[0] == b'-' { @@ -74,21 +74,19 @@ impl ArrayInput { None } - pub fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { - let values = to_array(values); - + pub fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { for (k, v) in &self.parameters { // if (!\is_int($k)) { $v = $k; } - let v: PhpMixed = match k { - PhpMixed::Int(_) => v.clone(), - _ => k.clone(), + let v = match k { + ParameterName::Index(_) => v.as_string(), + ParameterName::Name(k) => Some(k.as_str()), }; - if only_params && matches!(&v, PhpMixed::String(s) if s == "--") { + if only_params && v == Some("--") { return false; } - if values.iter().any(|x| x == &v) { + if v.is_some_and(|v| values.contains(&v)) { return true; } } @@ -98,31 +96,29 @@ impl ArrayInput { pub fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { - let values = to_array(values); - + ) -> InputValue { for (k, v) in &self.parameters { // $onlyParams && ('--' === $k || (\is_int($k) && '--' === $v)) if only_params { - let k_is_double_dash = matches!(k, PhpMixed::String(s) if s == "--"); + let k_is_double_dash = matches!(k, ParameterName::Name(k) if k == "--"); let int_v_double_dash = - matches!(k, PhpMixed::Int(_)) && matches!(v, PhpMixed::String(s) if s == "--"); + matches!(k, ParameterName::Index(_)) && v.as_string() == Some("--"); if k_is_double_dash || int_v_double_dash { return default; } } match k { - PhpMixed::Int(_) => { - if values.iter().any(|x| x == v) { - return PhpMixed::Bool(true); + ParameterName::Index(_) => { + if v.as_string().is_some_and(|v| values.contains(&v)) { + return InputValue::Bool(true); } } - _ => { - if values.iter().any(|x| x == k) { + ParameterName::Name(k) => { + if values.contains(&k.as_str()) { return v.clone(); } } @@ -136,16 +132,17 @@ impl ArrayInput { // Clone to avoid borrowing self while mutating; PHP iterates over a copy semantically. let parameters = self.parameters.clone(); for (key, value) in parameters { - let key = shirabe_php_shim::php_to_string(&key); - if key == "--" { + // PHP compares the key as a string even when it is an int. + let key_string = key.to_string(); + if key_string == "--" { return Ok(()); } - if key.starts_with("--") { - self.add_long_option(&shirabe_php_shim::substr(&key, 2, None), value)?; - } else if key.starts_with("-") { - self.add_short_option(&shirabe_php_shim::substr(&key, 1, None), value)?; + if key_string.starts_with("--") { + self.add_long_option(&shirabe_php_shim::substr(&key_string, 2, None), value)?; + } else if key_string.starts_with("-") { + self.add_short_option(&shirabe_php_shim::substr(&key_string, 1, None), value)?; } else { - self.add_argument(&PhpMixed::String(key), value)?; + self.add_argument(&key.to_argument_name(), value)?; } } @@ -153,7 +150,7 @@ impl ArrayInput { } /// Adds a short option value. - fn add_short_option(&mut self, shortcut: &str, value: PhpMixed) -> anyhow::Result<()> { + fn add_short_option(&mut self, shortcut: &str, value: InputValue) -> anyhow::Result<()> { if !self.inner.definition.has_shortcut(shortcut) { return Err(InvalidOptionException::new(format!( "The \"-{}\" option does not exist.", @@ -172,7 +169,7 @@ impl ArrayInput { } /// Adds a long option value. - fn add_long_option(&mut self, name: &str, mut value: PhpMixed) -> anyhow::Result<()> { + fn add_long_option(&mut self, name: &str, mut value: InputValue) -> anyhow::Result<()> { if !self.inner.definition.has_option(name) { if !self.inner.definition.has_negation(name) { return Err(InvalidOptionException::new(format!( @@ -185,14 +182,14 @@ impl ArrayInput { let option_name = self.inner.definition.negation_to_name(name)?; self.inner .options - .insert(option_name, PhpMixed::Bool(false)); + .insert(option_name, InputValue::Bool(false)); return Ok(()); } let option = self.inner.definition.get_option(name)?; - if matches!(value, PhpMixed::Null) { + if value.is_null() { if option.is_value_required() { return Err(InvalidOptionException::new(format!( "The \"--{}\" option requires a value.", @@ -202,7 +199,7 @@ impl ArrayInput { } if !option.is_value_optional() { - value = PhpMixed::Bool(true); + value = InputValue::Bool(true); } } @@ -212,18 +209,16 @@ impl ArrayInput { } /// Adds an argument value. - fn add_argument(&mut self, name: &PhpMixed, value: PhpMixed) -> anyhow::Result<()> { + fn add_argument(&mut self, name: &ArgumentName, value: InputValue) -> anyhow::Result<()> { if !self.inner.definition.has_argument(name) { return Err(InvalidArgumentException::new(format!( "The \"{}\" argument does not exist.", - name.clone() + name )) .into()); } - self.inner - .arguments - .insert(shirabe_php_shim::php_to_string(name), value); + self.inner.arguments.insert(name.to_string(), value); Ok(()) } @@ -235,30 +230,29 @@ impl std::fmt::Display for ArrayInput { let mut params: Vec = vec![]; for (param, val) in &self.parameters { // $param && \is_string($param) && '-' === $param[0] - let is_option_key = - matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-'); - if is_option_key { - let param = param.as_string().unwrap(); + if let ParameterName::Name(param) = param + && !param.is_empty() + && param.as_bytes()[0] == b'-' + { let glue = if param.as_bytes().get(1) == Some(&b'-') { "=" } else { " " }; - if let PhpMixed::List(list) = val { + if let InputValue::Array(list) = val { for v in list { - let v = shirabe_php_shim::php_to_string(v); params.push(format!( "{}{}", param, if !v.is_empty() { - format!("{}{}", glue, self.inner.escape_token(&v)) + format!("{}{}", glue, self.inner.escape_token(v)) } else { String::new() } )); } } else { - let val = shirabe_php_shim::php_to_string(val); + let val = val.to_php_string(); params.push(format!( "{}{}", param, @@ -269,17 +263,12 @@ impl std::fmt::Display for ArrayInput { } )); } - } else if let PhpMixed::List(list) = val { - let escaped: Vec = list - .iter() - .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v))) - .collect(); + } else if let InputValue::Array(list) = val { + let escaped: Vec = + list.iter().map(|v| self.inner.escape_token(v)).collect(); params.push(shirabe_php_shim::implode(" ", &escaped)); } else { - params.push( - self.inner - .escape_token(&shirabe_php_shim::php_to_string(val)), - ); + params.push(self.inner.escape_token(&val.to_php_string())); } } @@ -293,19 +282,19 @@ impl InputInterface for ArrayInput { } fn get_first_argument(&self) -> Option { - ArrayInput::get_first_argument(self).map(|v| shirabe_php_shim::php_to_string(&v)) + ArrayInput::get_first_argument(self).map(|v| v.to_php_string()) } - fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { + fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { ArrayInput::has_parameter_option(self, values, only_params) } fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { + ) -> InputValue { ArrayInput::get_parameter_option(self, values, default, only_params) } @@ -317,15 +306,15 @@ impl InputInterface for ArrayInput { self.inner.validate() } - fn get_arguments(&self) -> IndexMap { + fn get_arguments(&self) -> IndexMap { self.inner.get_arguments() } - fn get_argument(&self, name: &str) -> anyhow::Result { + fn get_argument(&self, name: &str) -> anyhow::Result { self.inner.get_argument(name) } - fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_argument(name, value) } @@ -333,15 +322,15 @@ impl InputInterface for ArrayInput { self.inner.has_argument(name) } - fn get_options(&self) -> IndexMap { + fn get_options(&self) -> IndexMap { self.inner.get_options() } - fn get_option(&self, name: &str) -> anyhow::Result { + fn get_option(&self, name: &str) -> anyhow::Result { self.inner.get_option(name) } - fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_option(name, value) } @@ -376,12 +365,32 @@ impl StreamableInputInterface for ArrayInput { } } -/// PHP `(array) $values` cast: a string becomes a single-element array. -fn to_array(values: PhpMixed) -> Vec { - match values { - PhpMixed::List(list) => list.into_iter().collect(), - PhpMixed::Array(array) => array.into_iter().map(|(_, v)| v).collect(), - PhpMixed::Null => vec![], - other => vec![other], +/// The `string|int` key of an [`ArrayInput`] parameter: a named argument, a named option +/// (`--foo` / `-f`), or the position of a bare token. +#[derive(Debug, Clone, PartialEq)] +pub enum ParameterName { + Name(String), + Index(i64), +} + +impl ParameterName { + pub fn of(name: &str) -> Self { + Self::Name(name.to_string()) + } + + fn to_argument_name(&self) -> ArgumentName { + match self { + Self::Name(name) => ArgumentName::Name(name.clone()), + Self::Index(index) => ArgumentName::Position(*index), + } + } +} + +impl std::fmt::Display for ParameterName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Name(name) => write!(f, "{}", name), + Self::Index(index) => write!(f, "{}", index), + } } } diff --git a/crates/shirabe-symfony-console/src/input/input.rs b/crates/shirabe-symfony-console/src/input/input.rs index 795e9c81..44519698 100644 --- a/crates/shirabe-symfony-console/src/input/input.rs +++ b/crates/shirabe-symfony-console/src/input/input.rs @@ -2,10 +2,11 @@ use crate::exception::InvalidArgumentException; use crate::exception::RuntimeException; +use crate::input::ArgumentName; use crate::input::InputDefinition; -use crate::input::InputOptionValue; +use crate::input::InputValue; use indexmap::IndexMap; -use shirabe_php_shim::{PhpMixed, PhpResource, php_regex, preg_is_match}; +use shirabe_php_shim::{PhpResource, php_regex, preg_is_match}; /// Input is the base class for all concrete Input classes. /// @@ -18,8 +19,8 @@ use shirabe_php_shim::{PhpMixed, PhpResource, php_regex, preg_is_match}; pub struct Input { pub(crate) definition: InputDefinition, stream: Option, - pub(crate) options: IndexMap, - pub(crate) arguments: IndexMap, + pub(crate) options: IndexMap, + pub(crate) arguments: IndexMap, interactive: bool, } @@ -74,7 +75,7 @@ impl Input { |argument: &String| { !given_arguments.contains_key(argument) && definition - .get_argument(&PhpMixed::String(argument.clone())) + .get_argument(&ArgumentName::of(argument)) .map(|a| a.is_required()) .unwrap_or(false) }, @@ -99,18 +100,15 @@ impl Input { self.interactive = interactive; } - pub fn get_arguments(&self) -> IndexMap { + pub fn get_arguments(&self) -> IndexMap { shirabe_php_shim::array_merge_map( self.definition.get_argument_defaults(), self.arguments.clone(), ) } - pub fn get_argument(&self, name: &str) -> anyhow::Result { - if !self - .definition - .has_argument(&PhpMixed::String(name.to_string())) - { + pub fn get_argument(&self, name: &str) -> anyhow::Result { + if !self.definition.has_argument(&ArgumentName::of(name)) { return Err(InvalidArgumentException::new(format!( "The \"{}\" argument does not exist.", name @@ -122,17 +120,14 @@ impl Input { Some(value) => value.clone(), None => self .definition - .get_argument(&PhpMixed::String(name.to_string()))? + .get_argument(&ArgumentName::of(name))? .get_default() .clone(), }) } - pub fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { - if !self - .definition - .has_argument(&PhpMixed::String(name.to_string())) - { + pub fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { + if !self.definition.has_argument(&ArgumentName::of(name)) { return Err(InvalidArgumentException::new(format!( "The \"{}\" argument does not exist.", name @@ -146,25 +141,24 @@ impl Input { } pub fn has_argument(&self, name: &str) -> bool { - self.definition - .has_argument(&PhpMixed::String(name.to_string())) + self.definition.has_argument(&ArgumentName::of(name)) } - pub fn get_options(&self) -> IndexMap { + pub fn get_options(&self) -> IndexMap { shirabe_php_shim::array_merge_map( self.definition.get_option_defaults(), self.options.clone(), ) } - pub fn get_option(&self, name: &str) -> anyhow::Result { + pub fn get_option(&self, name: &str) -> anyhow::Result { if self.definition.has_negation(name) { let value = self.get_option(&self.definition.negation_to_name(name)?)?; if value.is_null() { return Ok(value); } - return Ok(InputOptionValue::Bool(!value.to_bool())); + return Ok(InputValue::Bool(!value.to_bool())); } if !self.definition.has_option(name) { @@ -176,18 +170,17 @@ impl Input { } Ok(if let Some(value) = self.options.get(name) { - InputOptionValue::from_php_mixed(value) + value.clone() } else { - let option = self.definition.get_option(name)?; - InputOptionValue::from_php_mixed(option.get_default()) + self.definition.get_option(name)?.get_default().clone() }) } - pub fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + pub fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { if self.definition.has_negation(name) { let negated = self.definition.negation_to_name(name)?; self.options - .insert(negated, PhpMixed::Bool(!value.as_bool().unwrap_or(false))); + .insert(negated, InputValue::Bool(!value.to_bool())); return Ok(()); } else if !self.definition.has_option(name) { diff --git a/crates/shirabe-symfony-console/src/input/input_argument.rs b/crates/shirabe-symfony-console/src/input/input_argument.rs index 5ca91a6b..ef07af0e 100644 --- a/crates/shirabe-symfony-console/src/input/input_argument.rs +++ b/crates/shirabe-symfony-console/src/input/input_argument.rs @@ -2,13 +2,13 @@ use crate::exception::InvalidArgumentException; use crate::exception::LogicException; -use shirabe_php_shim::PhpMixed; +use crate::input::InputValue; #[derive(Debug, Clone)] pub struct InputArgument { name: String, mode: i64, - default: PhpMixed, + default: InputValue, description: String, } @@ -21,7 +21,7 @@ impl InputArgument { name: String, mode: Option, description: String, - default: PhpMixed, + default: InputValue, ) -> anyhow::Result { let mode = match mode { None => Self::OPTIONAL, @@ -39,7 +39,7 @@ impl InputArgument { name, mode, description, - default: PhpMixed::Null, + default: InputValue::Null, }; argument.set_default(default)?; @@ -59,8 +59,8 @@ impl InputArgument { Self::IS_ARRAY == (Self::IS_ARRAY & self.mode) } - pub fn set_default(&mut self, default: PhpMixed) -> anyhow::Result<()> { - if self.is_required() && !matches!(default, PhpMixed::Null) { + pub fn set_default(&mut self, default: InputValue) -> anyhow::Result<()> { + if self.is_required() && !default.is_null() { return Err(LogicException::new( "Cannot set a default value except for InputArgument::OPTIONAL mode.".to_string(), ) @@ -69,8 +69,8 @@ impl InputArgument { let default = if self.is_array() { match default { - PhpMixed::Null => PhpMixed::List(vec![]), - PhpMixed::List(_) => default, + InputValue::Null => InputValue::Array(vec![]), + InputValue::Array(_) => default, _ => { return Err(LogicException::new( "A default value for an array argument must be an array.".to_string(), @@ -86,7 +86,7 @@ impl InputArgument { Ok(()) } - pub fn get_default(&self) -> &PhpMixed { + pub fn get_default(&self) -> &InputValue { &self.default } diff --git a/crates/shirabe-symfony-console/src/input/input_definition.rs b/crates/shirabe-symfony-console/src/input/input_definition.rs index 8d116bc1..35fa4e52 100644 --- a/crates/shirabe-symfony-console/src/input/input_definition.rs +++ b/crates/shirabe-symfony-console/src/input/input_definition.rs @@ -4,8 +4,8 @@ use crate::exception::InvalidArgumentException; use crate::exception::LogicException; use crate::input::InputArgument; use crate::input::InputOption; +use crate::input::InputValue; use indexmap::IndexMap; -use shirabe_php_shim::PhpMixed; /// A InputDefinition represents a set of valid command line arguments and options. /// @@ -154,40 +154,32 @@ impl InputDefinition { } /// Returns an InputArgument by name or by position. - pub fn get_argument(&self, name: &PhpMixed) -> anyhow::Result> { + pub fn get_argument(&self, name: &ArgumentName) -> anyhow::Result> { if !self.has_argument(name) { return Err(InvalidArgumentException::new(format!( "The \"{}\" argument does not exist.", - name.clone() + name )) .into()); } match name { - PhpMixed::Int(index) => { + ArgumentName::Position(index) => { let arguments: Vec> = self.arguments.values().cloned().collect(); Ok(std::rc::Rc::clone(&arguments[*index as usize])) } - _ => { - let key = shirabe_php_shim::php_to_string(name); - Ok(std::rc::Rc::clone(&self.arguments[&key])) - } + ArgumentName::Name(name) => Ok(std::rc::Rc::clone(&self.arguments[name])), } } /// Returns true if an InputArgument object exists by name or position. - pub fn has_argument(&self, name: &PhpMixed) -> bool { + pub fn has_argument(&self, name: &ArgumentName) -> bool { match name { - PhpMixed::Int(index) => { - let arguments: Vec> = - self.arguments.values().cloned().collect(); - *index >= 0 && (*index as usize) < arguments.len() - } - _ => { - let key = shirabe_php_shim::php_to_string(name); - self.arguments.contains_key(&key) + ArgumentName::Position(index) => { + *index >= 0 && (*index as usize) < self.arguments.len() } + ArgumentName::Name(name) => self.arguments.contains_key(name), } } @@ -210,7 +202,7 @@ impl InputDefinition { self.required_count } - pub fn get_argument_defaults(&self) -> IndexMap { + pub fn get_argument_defaults(&self) -> IndexMap { let mut values = IndexMap::new(); for argument in self.arguments.values() { values.insert( @@ -346,7 +338,7 @@ impl InputDefinition { self.get_option(&self.shortcut_to_name(shortcut)?) } - pub fn get_option_defaults(&self) -> IndexMap { + pub fn get_option_defaults(&self) -> IndexMap { let mut values = IndexMap::new(); for option in self.options.values() { values.insert(option.get_name().to_string(), option.get_default().clone()); @@ -448,3 +440,26 @@ impl InputDefinition { format!("{}{}", shirabe_php_shim::implode(" ", &elements), tail) } } + +/// The `string|int` selector [`InputDefinition::get_argument`] and +/// [`InputDefinition::has_argument`] accept. +#[derive(Debug, Clone)] +pub enum ArgumentName { + Name(String), + Position(i64), +} + +impl ArgumentName { + pub fn of(name: &str) -> Self { + Self::Name(name.to_string()) + } +} + +impl std::fmt::Display for ArgumentName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Name(name) => write!(f, "{}", name), + Self::Position(index) => write!(f, "{}", index), + } + } +} diff --git a/crates/shirabe-symfony-console/src/input/input_interface.rs b/crates/shirabe-symfony-console/src/input/input_interface.rs index 3c3ec2d5..85cd5e7f 100644 --- a/crates/shirabe-symfony-console/src/input/input_interface.rs +++ b/crates/shirabe-symfony-console/src/input/input_interface.rs @@ -1,9 +1,8 @@ //! ref: composer/vendor/symfony/console/Input/InputInterface.php use crate::input::InputDefinition; -use crate::input::InputOptionValue; +use crate::input::InputValue; use crate::input::StreamableInputInterface; -use shirabe_php_shim::PhpMixed; pub trait InputInterface: std::fmt::Debug + shirabe_php_shim::AsAny { /// Models PHP's `clone` operatior. @@ -11,32 +10,32 @@ pub trait InputInterface: std::fmt::Debug + shirabe_php_shim::AsAny { fn get_first_argument(&self) -> Option; - fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool; + fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool; fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed; + ) -> InputValue; fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()>; fn validate(&mut self) -> anyhow::Result<()>; - fn get_arguments(&self) -> indexmap::IndexMap; + fn get_arguments(&self) -> indexmap::IndexMap; - fn get_argument(&self, name: &str) -> anyhow::Result; + fn get_argument(&self, name: &str) -> anyhow::Result; - fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()>; fn has_argument(&self, name: &str) -> bool; - fn get_options(&self) -> indexmap::IndexMap; + fn get_options(&self) -> indexmap::IndexMap; - fn get_option(&self, name: &str) -> anyhow::Result; + fn get_option(&self, name: &str) -> anyhow::Result; - fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()>; fn has_option(&self, name: &str) -> bool; diff --git a/crates/shirabe-symfony-console/src/input/input_option.rs b/crates/shirabe-symfony-console/src/input/input_option.rs index d37e18ae..6484d832 100644 --- a/crates/shirabe-symfony-console/src/input/input_option.rs +++ b/crates/shirabe-symfony-console/src/input/input_option.rs @@ -2,14 +2,15 @@ use crate::exception::InvalidArgumentException; use crate::exception::LogicException; -use shirabe_php_shim::{PhpMixed, php_regex, preg_split}; +use crate::input::InputValue; +use shirabe_php_shim::{php_regex, preg_split}; #[derive(Debug, Clone)] pub struct InputOption { name: String, shortcut: Option, mode: i64, - default: PhpMixed, + default: InputValue, description: String, } @@ -22,10 +23,10 @@ impl InputOption { pub fn new( name: &str, - shortcut: PhpMixed, + shortcut: Option<&str>, mode: Option, description: String, - default: PhpMixed, + default: InputValue, ) -> anyhow::Result { let name = if let Some(stripped) = name.strip_prefix("--") { stripped.to_string() @@ -41,26 +42,8 @@ impl InputOption { } 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, + None | Some("") => None, + Some(shortcut) => Self::normalize_shortcut(shortcut)?, }; let mode = match mode { @@ -80,7 +63,7 @@ impl InputOption { shortcut, mode, description, - default: PhpMixed::Null, + default: InputValue::Null, }; if option.is_array() && !option.accept_value() { @@ -97,8 +80,8 @@ impl InputOption { Ok(option) } - fn normalize_shortcut(s: String) -> anyhow::Result> { - let stripped = shirabe_php_shim::ltrim(&s, Some("-")); + fn normalize_shortcut(s: &str) -> anyhow::Result> { + let stripped = shirabe_php_shim::ltrim(s, Some("-")); let parts = preg_split(php_regex!(r"{(\|)-?}"), &stripped); let filtered: Vec = shirabe_php_shim::array_filter(&parts, |s: &String| !s.is_empty()); @@ -140,9 +123,8 @@ impl InputOption { 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) - { + pub fn set_default(&mut self, default: InputValue) -> anyhow::Result<()> { + if Self::VALUE_NONE == (Self::VALUE_NONE & self.mode) && !default.is_null() { return Err(LogicException::new( "Cannot set a default value when using InputOption::VALUE_NONE mode.".to_string(), ) @@ -151,9 +133,8 @@ impl InputOption { 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, + InputValue::Null => InputValue::Array(vec![]), + InputValue::Array(_) => default, _ => { return Err(LogicException::new( "A default value for an array option must be an array.".to_string(), @@ -168,12 +149,12 @@ impl InputOption { self.default = if self.accept_value() || self.is_negatable() { default } else { - PhpMixed::Bool(false) + InputValue::Bool(false) }; Ok(()) } - pub fn get_default(&self) -> &PhpMixed { + pub fn get_default(&self) -> &InputValue { &self.default } @@ -191,90 +172,3 @@ impl InputOption { && option.is_value_optional() == self.is_value_optional() } } - -/// The `bool|string|string[]|null` domain of a parsed option value, as returned by -/// [`InputInterface::get_option`](crate::input::InputInterface::get_option). -#[derive(Debug, Clone, PartialEq)] -pub enum InputOptionValue { - Null, - Bool(bool), - String(String), - Array(Vec), -} - -impl InputOptionValue { - /// Narrows a raw option value to this domain. - /// - /// TODO(type-model): `Input` keeps parsed options and `InputOption` defaults as `PhpMixed`, so - /// a value outside this domain — an int, a float, or an array holding one — can only be - /// rejected here. - pub(crate) fn from_php_mixed(value: &PhpMixed) -> Self { - match value { - PhpMixed::Null => Self::Null, - PhpMixed::Bool(b) => Self::Bool(*b), - PhpMixed::String(s) => Self::String(s.clone()), - PhpMixed::List(_) | PhpMixed::Array(_) => Self::Array( - value - .values() - .into_iter() - .map(|item| match item { - PhpMixed::String(s) => s.clone(), - other => panic!("an option array holds {:?}, not a string", other), - }) - .collect(), - ), - other => panic!( - "an option holds {:?}, not a bool, string, array or null", - other - ), - } - } - - pub fn is_null(&self) -> bool { - matches!(self, Self::Null) - } - - pub fn as_bool(&self) -> Option { - match self { - Self::Bool(b) => Some(*b), - _ => None, - } - } - - pub fn as_string(&self) -> Option<&str> { - match self { - Self::String(s) => Some(s.as_str()), - _ => None, - } - } - - pub fn as_array(&self) -> Option<&[String]> { - match self { - Self::Array(items) => Some(items), - _ => None, - } - } - - /// PHP loose boolean cast `(bool) $value`. - pub fn to_bool(&self) -> bool { - match self { - Self::Null => false, - Self::Bool(b) => *b, - Self::String(s) => !s.is_empty() && s != "0", - Self::Array(items) => !items.is_empty(), - } - } -} - -impl From for PhpMixed { - fn from(value: InputOptionValue) -> Self { - match value { - InputOptionValue::Null => PhpMixed::Null, - InputOptionValue::Bool(b) => PhpMixed::Bool(b), - InputOptionValue::String(s) => PhpMixed::String(s), - InputOptionValue::Array(items) => { - PhpMixed::List(items.into_iter().map(PhpMixed::String).collect()) - } - } - } -} diff --git a/crates/shirabe-symfony-console/src/input/input_value.rs b/crates/shirabe-symfony-console/src/input/input_value.rs new file mode 100644 index 00000000..288c55d6 --- /dev/null +++ b/crates/shirabe-symfony-console/src/input/input_value.rs @@ -0,0 +1,146 @@ +//! ref: composer/vendor/symfony/console/Input/InputInterface.php + +/// The value an option or an argument can hold. +/// +/// Symfony declares the domain as `string|bool|int|float|array|null` and only ever narrows it in +/// PHPDoc; the arrays it stores are lists of strings. +#[derive(Debug, Clone, PartialEq)] +pub enum InputValue { + Null, + Bool(bool), + String(String), + Array(Vec), +} + +impl InputValue { + /// Narrows a PHP value to this domain. + /// + /// TODO(type-model): the question helpers and the plugin bridge still hand back a + /// `PhpMixed`, so a value outside this domain can only be rejected here. + pub fn from_php_mixed(value: &shirabe_php_shim::PhpMixed) -> Self { + use shirabe_php_shim::PhpMixed; + match value { + PhpMixed::Null => Self::Null, + PhpMixed::Bool(b) => Self::Bool(*b), + PhpMixed::String(s) => Self::String(s.clone()), + PhpMixed::List(_) | PhpMixed::Array(_) => Self::Array( + value + .values() + .into_iter() + .map(|item| match item { + PhpMixed::String(s) => s.clone(), + other => panic!("an input array holds {:?}, not a string", other), + }) + .collect(), + ), + other => panic!( + "an input value holds {:?}, not a bool, string, array or null", + other + ), + } + } + + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } + + pub fn is_array(&self) -> bool { + matches!(self, Self::Array(_)) + } + + pub fn as_bool(&self) -> Option { + match self { + Self::Bool(b) => Some(*b), + _ => None, + } + } + + pub fn as_string(&self) -> Option<&str> { + match self { + Self::String(s) => Some(s.as_str()), + _ => None, + } + } + + pub fn as_array(&self) -> Option<&[String]> { + match self { + Self::Array(items) => Some(items), + _ => None, + } + } + + /// PHP's `(bool) $value`. + pub fn to_bool(&self) -> bool { + match self { + Self::Null => false, + Self::Bool(b) => *b, + Self::String(s) => !s.is_empty() && s != "0", + Self::Array(items) => !items.is_empty(), + } + } + + /// The value as a PHP `mixed`, for the shim functions that take one. + pub fn to_php_mixed(&self) -> shirabe_php_shim::PhpMixed { + self.clone().into() + } + + /// PHP's `(string) $value`, which is a fatal error for an array. + pub fn to_php_string(&self) -> String { + match self { + Self::Null => String::new(), + Self::Bool(b) => { + if *b { + "1".to_string() + } else { + String::new() + } + } + Self::String(s) => s.clone(), + Self::Array(_) => panic!("array to string conversion"), + } + } +} + +impl From for shirabe_php_shim::PhpMixed { + fn from(value: InputValue) -> Self { + match value { + InputValue::Null => Self::Null, + InputValue::Bool(b) => Self::Bool(b), + InputValue::String(s) => Self::String(s), + InputValue::Array(items) => Self::List(items.into_iter().map(Self::String).collect()), + } + } +} + +impl From<&str> for InputValue { + fn from(value: &str) -> Self { + Self::String(value.to_string()) + } +} + +impl From for InputValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for InputValue { + fn from(value: bool) -> Self { + Self::Bool(value) + } +} + +impl From> for InputValue { + fn from(value: Option) -> Self { + match value { + Some(value) => Self::String(value), + None => Self::Null, + } + } +} + +impl From> for InputValue { + fn from(value: Vec) -> Self { + Self::Array(value) + } +} diff --git a/crates/shirabe-symfony-console/src/input/string_input.rs b/crates/shirabe-symfony-console/src/input/string_input.rs index 9fcb3fc8..09cee120 100644 --- a/crates/shirabe-symfony-console/src/input/string_input.rs +++ b/crates/shirabe-symfony-console/src/input/string_input.rs @@ -4,10 +4,10 @@ use crate::exception::InvalidArgumentException; use crate::input::ArgvInput; use crate::input::InputDefinition; use crate::input::InputInterface; -use crate::input::InputOptionValue; +use crate::input::InputValue; use crate::input::StreamableInputInterface; use indexmap::IndexMap; -use shirabe_php_shim::{PhpMixed, php_regex, preg_match}; +use shirabe_php_shim::{php_regex, preg_match}; /// StringInput represents an input provided as a string. /// @@ -137,16 +137,16 @@ impl InputInterface for StringInput { self.inner.get_first_argument() } - fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { + fn has_parameter_option(&self, values: &[&str], only_params: bool) -> bool { InputInterface::has_parameter_option(&self.inner, values, only_params) } fn get_parameter_option( &self, - values: PhpMixed, - default: PhpMixed, + values: &[&str], + default: InputValue, only_params: bool, - ) -> PhpMixed { + ) -> InputValue { InputInterface::get_parameter_option(&self.inner, values, default, only_params) } @@ -158,15 +158,15 @@ impl InputInterface for StringInput { self.inner.validate() } - fn get_arguments(&self) -> IndexMap { + fn get_arguments(&self) -> IndexMap { InputInterface::get_arguments(&self.inner) } - fn get_argument(&self, name: &str) -> anyhow::Result { + fn get_argument(&self, name: &str) -> anyhow::Result { self.inner.get_argument(name) } - fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_argument(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_argument(name, value) } @@ -174,15 +174,15 @@ impl InputInterface for StringInput { self.inner.has_argument(name) } - fn get_options(&self) -> IndexMap { + fn get_options(&self) -> IndexMap { InputInterface::get_options(&self.inner) } - fn get_option(&self, name: &str) -> anyhow::Result { + fn get_option(&self, name: &str) -> anyhow::Result { self.inner.get_option(name) } - fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { + fn set_option(&mut self, name: &str, value: InputValue) -> anyhow::Result<()> { self.inner.set_option(name, value) } -- cgit v1.3.1-4-g156e