diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-18 08:37:30 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-18 08:37:30 +0900 |
| commit | 4856f05e870ec5d2daaa74bf9a86a1519a813614 (patch) | |
| tree | ab81b7a9d59340c4d05ecaf3b6b1a81106cc8355 /crates/shirabe-symfony-console/src/input/input_option.rs | |
| parent | 5aaa92f760a975c6617b5d1fb7af8e58fed127b7 (diff) | |
| download | php-shirabe-4856f05e870ec5d2daaa74bf9a86a1519a813614.tar.gz php-shirabe-4856f05e870ec5d2daaa74bf9a86a1519a813614.tar.zst php-shirabe-4856f05e870ec5d2daaa74bf9a86a1519a813614.zip | |
fix(input): return a bool|string|string[]|null enum from get_option
`InputInterface::get_option` returned `PhpMixed`, so the negation branch
of `Input::get_option` reproduced PHP's `return !$value;` as
`!value.as_bool().unwrap_or(false)`, which inverts the result for a
string value instead of leaving it `false`. It now returns
`InputOptionValue`, whose `to_bool` is PHP's truthiness cast.
The narrowing also removes the `Vec<PhpMixed>` element handling the
commands carried for array options: `as_array` hands back `&[String]`,
so the `filter_map(|v| v.as_string())` chains at eight call sites
collapse. Its other accessors keep `PhpMixed`'s names and meanings
(`is_null`, `as_bool`, `as_string`, `to_bool`), and
`From<InputOptionValue> for PhpMixed` covers the callers that feed the
value back into an `IndexMap<String, PhpMixed>` or a `PhpMixed`
parameter.
`Input` keeps its parsed options and `InputOption` its defaults as
`PhpMixed`, so `Input::get_option` is where the narrowing happens and
where a value outside the domain panics.
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.rs | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/input/input_option.rs b/crates/shirabe-symfony-console/src/input/input_option.rs index 3b24d914..d37e18ae 100644 --- a/crates/shirabe-symfony-console/src/input/input_option.rs +++ b/crates/shirabe-symfony-console/src/input/input_option.rs @@ -191,3 +191,90 @@ 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()) + } + } + } +} |
