aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/input/input_option.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-19 23:45:37 +0900
committernsfisis <nsfisis@gmail.com>2026-08-19 23:45:37 +0900
commit0800c90a7ec0bc7a3d4c13063dfe6d2298a557bc (patch)
treeba5602549ce20b8d71136423e85dffc25deafff4 /crates/shirabe-symfony-console/src/input/input_option.rs
parent6ed3a85dd636366d194c9810fd777db7f25e263f (diff)
downloadphp-shirabe-0800c90a7ec0bc7a3d4c13063dfe6d2298a557bc.tar.gz
php-shirabe-0800c90a7ec0bc7a3d4c13063dfe6d2298a557bc.tar.zst
php-shirabe-0800c90a7ec0bc7a3d4c13063dfe6d2298a557bc.zip
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) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-symfony-console/src/input/input_option.rs')
-rw-r--r--crates/shirabe-symfony-console/src/input/input_option.rs138
1 files changed, 16 insertions, 122 deletions
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<String>,
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<i64>,
description: String,
- default: PhpMixed,
+ default: InputValue,
) -> anyhow::Result<Self> {
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<String> = 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<Option<String>> {
- let stripped = shirabe_php_shim::ltrim(&s, Some("-"));
+ fn normalize_shortcut(s: &str) -> anyhow::Result<Option<String>> {
+ let stripped = shirabe_php_shim::ltrim(s, Some("-"));
let parts = preg_split(php_regex!(r"{(\|)-?}"), &stripped);
let filtered: Vec<String> =
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<String>),
-}
-
-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<bool> {
- 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<InputOptionValue> 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())
- }
- }
- }
-}