aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/input/input_option.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-18 08:37:30 +0900
committernsfisis <nsfisis@gmail.com>2026-08-18 08:37:30 +0900
commit4856f05e870ec5d2daaa74bf9a86a1519a813614 (patch)
treeab81b7a9d59340c4d05ecaf3b6b1a81106cc8355 /crates/shirabe-symfony-console/src/input/input_option.rs
parent5aaa92f760a975c6617b5d1fb7af8e58fed127b7 (diff)
downloadphp-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.rs87
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())
+ }
+ }
+ }
+}