diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-22 18:54:10 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-22 18:54:10 +0900 |
| commit | 6195e33dfc87cf4b231006b3e5e686a9d8fe2dc1 (patch) | |
| tree | bd97be1b9307eb78ef1d4e3d9ef9d5a0dc797a3f /crates/shirabe/src/plugin/php_plugin_proxy.rs | |
| parent | 62c206032a09f6b19777c1b3bc6f4ca3da71cdc3 (diff) | |
| download | php-shirabe-6195e33dfc87cf4b231006b3e5e686a9d8fe2dc1.tar.gz php-shirabe-6195e33dfc87cf4b231006b3e5e686a9d8fe2dc1.tar.zst php-shirabe-6195e33dfc87cf4b231006b3e5e686a9d8fe2dc1.zip | |
refactor(plugin): decode RPC values through conversion traits
The R-table dispatchers hand-rolled argument decoding and return encoding per
method, spread over 24 helpers. `FromPluginArg` and `ToPluginValue` replace
them: blanket impls for `Option<T>`, `Vec<T>` and `IndexMap<String, T>` compose
over a handful of leaf types, so a composite like `Vec<IndexMap<String, String>>`
needs no helper of its own, and the lossy UTF-8 conversion of a PHP string lives
in one impl instead of a dozen call sites.
Argument errors now read `{method} expects {expected} at position {position}`
rather than a message written per call site. `arg_or` takes its default for an
explicit null too, not only for an omitted argument; every parameter it serves
is declared non-nullable in the proxy stubs, so PHP never passes one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/plugin/php_plugin_proxy.rs')
| -rw-r--r-- | crates/shirabe/src/plugin/php_plugin_proxy.rs | 1476 |
1 files changed, 658 insertions, 818 deletions
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 9ebdbbff..c9d6986b 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -291,38 +291,17 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { ) -> Result<PluginValue, PhpThrow> { if rhandle == 0 { if method_name == "__shirabe_find_file" { - let class = match args.first() { - // TODO(bytes): lossy UTF-8; class names are bytes in PHP. - Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(), - other => { - return Err(runtime_throw(format!( - "__shirabe_find_file expects a class name argument, got {other:?}" - ))); - } - }; - return Ok(match find_file_in_registered_loaders(&class) { - Some(file) => PluginValue::string(file), - None => PluginValue::Null, - }); + let class = arg::<String>(method_name, &args, 0)?; + return Ok(find_file_in_registered_loaders(&class).to_plugin_value()); } if method_name == "__shirabe_run_rust_command" { - let (name, input_line) = match (args.first(), args.get(1)) { - (Some(PluginValue::String(name)), Some(PluginValue::String(line))) => ( - // TODO(bytes): lossy UTF-8; command lines are bytes in PHP. - String::from_utf8_lossy(name).into_owned(), - String::from_utf8_lossy(line).into_owned(), - ), - _ => { - return Err(runtime_throw(format!( - "__shirabe_run_rust_command expects a command name and an input line, got {args:?}" - ))); - } - }; + let name = arg::<String>(method_name, &args, 0)?; + let input_line = arg::<String>(method_name, &args, 1)?; return match crate::console::application::run_worker_reverse_command( &name, &input_line, ) { - Ok(code) => Ok(PluginValue::Int(code)), + Ok(code) => Ok(code.to_plugin_value()), Err(e) => Err(runtime_throw(format!("{e:#}"))), }; } @@ -416,17 +395,11 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT ))); } }; - let string_arg = |position: usize| -> Result<String, PhpThrow> { - match ctor_args.get(position) { - Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), - other => Err(runtime_throw(format!( - "{class} expects a string constructor argument at position {position}, got {other:?}" - ))), - } - }; + let string_arg = |position: usize| arg::<String>(&class, ctor_args, position); + let package_arg = |position: usize| arg::<PackageInterfaceHandle>(&class, ctor_args, position); let alias_package_arg = |position: usize| -> Result<crate::package::AliasPackageHandle, PhpThrow> { - package_from_arg(&class, ctor_args.get(position))? + package_arg(position)? .as_alias() .ok_or_else(|| runtime_throw(format!("{class} expects an AliasPackage"))) }; @@ -442,7 +415,7 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT // The alias target has to be a package that already lives on the Rust side; an alias of // an alias has no Rust representation, so its narrowing is an explicit error too. "Composer\\Package\\AliasPackage" => { - let alias_of = package_from_arg(&class, ctor_args.first())? + let alias_of = package_arg(0)? .as_package() .ok_or_else(|| runtime_throw(format!("{class} expects a real Package to alias")))?; AnyPackage::AliasPackage(crate::package::AliasPackage::new( @@ -452,11 +425,9 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT )) } "Composer\\Package\\CompleteAliasPackage" => { - let alias_of = package_from_arg(&class, ctor_args.first())? - .as_complete_package() - .ok_or_else(|| { - runtime_throw(format!("{class} expects a real CompletePackage to alias")) - })?; + let alias_of = package_arg(0)?.as_complete_package().ok_or_else(|| { + runtime_throw(format!("{class} expects a real CompletePackage to alias")) + })?; AnyPackage::CompleteAliasPackage(crate::package::CompleteAliasPackage::new( alias_of, string_arg(1)?, @@ -464,11 +435,9 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT )) } "Composer\\Package\\RootAliasPackage" => { - let alias_of = package_from_arg(&class, ctor_args.first())? - .as_root_package() - .ok_or_else(|| { - runtime_throw(format!("{class} expects a real RootPackage to alias")) - })?; + let alias_of = package_arg(0)?.as_root_package().ok_or_else(|| { + runtime_throw(format!("{class} expects a real RootPackage to alias")) + })?; AnyPackage::RootAliasPackage(crate::package::RootAliasPackage::new( alias_of, string_arg(1)?, @@ -479,26 +448,20 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT // one is a complete instance rather than a second view on a Rust-side service. "Composer\\DependencyResolver\\Operation\\InstallOperation" => { return Ok(operation_construction_result(AnyOperation::Install( - crate::dependency_resolver::operation::InstallOperation::new(package_from_arg( - &class, - ctor_args.first(), - )?), + crate::dependency_resolver::operation::InstallOperation::new(package_arg(0)?), ))); } "Composer\\DependencyResolver\\Operation\\UpdateOperation" => { return Ok(operation_construction_result(AnyOperation::Update( crate::dependency_resolver::operation::UpdateOperation::new( - package_from_arg(&class, ctor_args.first())?, - package_from_arg(&class, ctor_args.get(1))?, + package_arg(0)?, + package_arg(1)?, ), ))); } "Composer\\DependencyResolver\\Operation\\UninstallOperation" => { return Ok(operation_construction_result(AnyOperation::Uninstall( - crate::dependency_resolver::operation::UninstallOperation::new(package_from_arg( - &class, - ctor_args.first(), - )?), + crate::dependency_resolver::operation::UninstallOperation::new(package_arg(0)?), ))); } "Composer\\DependencyResolver\\Operation\\MarkAliasInstalledOperation" => { @@ -606,25 +569,20 @@ fn dispatch_property_access( method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - let property = required_string_arg(method_name, args.first())?; + let property = arg::<String>(method_name, args, 0)?; // `BasePackage::$id` is the one instance property the entities expose so far. if let RustEntity::Package(package) = entity && property == "id" { return match method_name { - "__get" => Ok(PluginValue::Int( - package.borrow().as_package_interface().get_id(), - )), + "__get" => Ok(package + .borrow() + .as_package_interface() + .get_id() + .to_plugin_value()), "__isset" => Ok(PluginValue::Bool(true)), "__set" => { - let id = match args.get(1) { - Some(PluginValue::Int(id)) => *id, - other => { - return Err(runtime_throw(format!( - "the package property `id` takes an int, got {other:?}" - ))); - } - }; + let id = arg::<i64>(method_name, args, 1)?; package.borrow_mut().as_package_interface_mut().set_id(id); Ok(PluginValue::Null) } @@ -657,7 +615,7 @@ fn dispatch_plugin_method( let capabilities = capable .get_capabilities() .map_err(|error| runtime_throw(format!("getCapabilities failed: {error:#}")))?; - Ok(PluginValue::from_php_mixed(&PhpMixed::Array(capabilities))) + Ok(capabilities.to_plugin_value()) } // TODO(plugin): the lifecycle methods would have to turn the `$composer`/`$io` stubs the // child passes back into the Rust handles they stand for; nothing calls them, because @@ -727,73 +685,38 @@ fn dispatch_config_method( method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - let key = |position: usize| -> Result<String, PhpThrow> { - match args.get(position) { - // TODO(bytes): lossy UTF-8; config keys are bytes in PHP. - Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), - other => Err(runtime_throw(format!( - "{method_name} expects a string key, got {other:?}" - ))), - } - }; - let flags = |position: usize| -> Result<i64, PhpThrow> { - match args.get(position) { - None | Some(PluginValue::Null) => Ok(0), - Some(PluginValue::Int(flags)) => Ok(*flags), - other => Err(runtime_throw(format!( - "{method_name} expects int flags, got {other:?}" - ))), - } - }; + let key = |position: usize| arg::<String>(method_name, args, position); + let flags = |position: usize| arg_or::<i64>(method_name, args, position, 0); match method_name { "get" => { let value = config .borrow() .get_with_flags(&key(0)?, flags(1)?) .map_err(|error| runtime_throw(format!("get failed over RPC: {error}")))?; - Ok(PluginValue::from_php_mixed(&value)) + Ok(value.to_plugin_value()) } "all" => { let all = config .borrow_mut() .all(flags(0)?) .map_err(|error| runtime_throw(format!("all failed over RPC: {error}")))?; - Ok(PluginValue::from_php_mixed(&PhpMixed::Array(all))) + Ok(all.to_plugin_value()) } - "raw" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array( - config.borrow().raw(), - ))), - "has" => Ok(PluginValue::Bool(config.borrow().has(&key(0)?))), - "getRepositories" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array( - config.borrow().get_repositories(), - ))), - "getSourceOfValue" => Ok(PluginValue::string( - config.borrow_mut().get_source_of_value(&key(0)?), - )), + "raw" => Ok(config.borrow().raw().to_plugin_value()), + "has" => Ok(config.borrow().has(&key(0)?).to_plugin_value()), + "getRepositories" => Ok(config.borrow().get_repositories().to_plugin_value()), + "getSourceOfValue" => Ok(config + .borrow_mut() + .get_source_of_value(&key(0)?) + .to_plugin_value()), "merge" => { - let values = match args.first().map(PluginValue::to_php_mixed).transpose() { - Ok(Some(PhpMixed::Array(values))) => values, - Ok(Some(PhpMixed::List(items))) if items.is_empty() => IndexMap::new(), - Ok(other) => { - return Err(runtime_throw(format!( - "merge expects a config array, got {other:?}" - ))); - } - Err(error) => { - return Err(runtime_throw(format!( - "merge could not decode its argument: {error:#}" - ))); - } - }; - let source = match args.get(1) { - None => crate::config::Config::SOURCE_UNKNOWN.to_string(), - Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(), - other => { - return Err(runtime_throw(format!( - "merge expects a string source, got {other:?}" - ))); - } - }; + let values = arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?; + let source = arg_or::<String>( + method_name, + args, + 1, + crate::config::Config::SOURCE_UNKNOWN.to_string(), + )?; config.borrow_mut().merge(&values, &source); Ok(PluginValue::Null) } @@ -817,25 +740,10 @@ fn dispatch_download_manager_method( method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - let string_arg = |position: usize| -> Result<String, PhpThrow> { - match args.get(position) { - // TODO(bytes): lossy UTF-8; paths and types are bytes in PHP. - Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), - other => Err(runtime_throw(format!( - "{method_name} expects a string argument at position {position}, got {other:?}" - ))), - } - }; + let string_arg = |position: usize| arg::<String>(method_name, args, position); match method_name { "setPreferSource" | "setPreferDist" => { - let preferred = match args.first() { - Some(PluginValue::Bool(preferred)) => *preferred, - other => { - return Err(runtime_throw(format!( - "{method_name} expects a bool, got {other:?}" - ))); - } - }; + let preferred = arg::<bool>(method_name, args, 0)?; if method_name == "setPreferSource" { dm.borrow_mut().set_prefer_source(preferred); } else { @@ -849,39 +757,39 @@ fn dispatch_download_manager_method( match method_name { "download" => { dm.download( - package_from_arg(method_name, args.first())?, + arg::<PackageInterfaceHandle>(method_name, args, 0)?, &string_arg(1)?, - optional_package_from_arg(method_name, args.get(2))?, + arg::<Option<PackageInterfaceHandle>>(method_name, args, 2)?, ) .await } "prepare" => { dm.prepare( &string_arg(0)?, - package_from_arg(method_name, args.get(1))?, + arg::<PackageInterfaceHandle>(method_name, args, 1)?, &string_arg(2)?, - optional_package_from_arg(method_name, args.get(3))?, + arg::<Option<PackageInterfaceHandle>>(method_name, args, 3)?, ) .await } "install" => { dm.install( - package_from_arg(method_name, args.first())?, + arg::<PackageInterfaceHandle>(method_name, args, 0)?, &string_arg(1)?, ) .await } "update" => { dm.update( - package_from_arg(method_name, args.first())?, - package_from_arg(method_name, args.get(1))?, + arg::<PackageInterfaceHandle>(method_name, args, 0)?, + arg::<PackageInterfaceHandle>(method_name, args, 1)?, &string_arg(2)?, ) .await } "remove" => { dm.remove( - package_from_arg(method_name, args.first())?, + arg::<PackageInterfaceHandle>(method_name, args, 0)?, &string_arg(1)?, ) .await @@ -889,9 +797,9 @@ fn dispatch_download_manager_method( _ => { dm.cleanup( &string_arg(0)?, - package_from_arg(method_name, args.get(1))?, + arg::<PackageInterfaceHandle>(method_name, args, 1)?, &string_arg(2)?, - optional_package_from_arg(method_name, args.get(3))?, + arg::<Option<PackageInterfaceHandle>>(method_name, args, 3)?, ) .await } @@ -902,11 +810,7 @@ fn dispatch_download_manager_method( runtime_throw(format!("{method_name} failed over RPC: {error:#}")) }) })?; - let value = match resolved { - Some(value) => PluginValue::from_php_mixed(&value), - None => PluginValue::Null, - }; - resolved_promise(value) + resolved_promise(resolved.to_plugin_value()) } // TODO(plugin): the downloader-facing surface (getDownloader / setDownloader / // getDownloaderForPackage / getDownloaderType) needs proxy stubs for @@ -938,70 +842,68 @@ fn dispatch_filesystem_method( method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - let string_arg = |position: usize| -> Result<String, PhpThrow> { - match args.get(position) { - // TODO(bytes): lossy UTF-8; paths are bytes in PHP. - Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), - other => Err(runtime_throw(format!( - "{method_name} expects a string argument at position {position}, got {other:?}" - ))), - } - }; + let string_arg = |position: usize| arg::<String>(method_name, args, position); // TODO(plugin): the exception class the real method throws (RuntimeException, IOException, // LogicException) is collapsed to RuntimeException on this side of the boundary. let failed = |error: anyhow::Error| runtime_throw(format!("{method_name} failed: {error:#}")); match method_name { - "remove" => Ok(PluginValue::Bool( - fs.borrow_mut().remove(string_arg(0)?).map_err(failed)?, - )), - "isDirEmpty" => Ok(PluginValue::Bool(fs.borrow().is_dir_empty(&string_arg(0)?))), + "remove" => Ok(fs + .borrow_mut() + .remove(string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), + "isDirEmpty" => Ok(fs.borrow().is_dir_empty(&string_arg(0)?).to_plugin_value()), "emptyDirectory" => { fs.borrow_mut() - .empty_directory(&string_arg(0)?, bool_arg(method_name, args.get(1))?) + .empty_directory(&string_arg(0)?, arg::<bool>(method_name, args, 1)?) .map_err(failed)?; Ok(PluginValue::Null) } - "removeDirectory" => Ok(PluginValue::Bool( - fs.borrow_mut() - .remove_directory(string_arg(0)?) - .map_err(failed)?, - )), + "removeDirectory" => Ok(fs + .borrow_mut() + .remove_directory(string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), "removeDirectoryAsync" => { let directory = string_arg(0)?; let removed = crate::util::sync_executor::block_on(async { crate::util::Filesystem::remove_directory_async_via(fs, &directory).await }) .map_err(failed)?; - resolved_promise(PluginValue::Bool(removed)) + resolved_promise(removed.to_plugin_value()) } - "removeDirectoryPhp" => Ok(PluginValue::Bool( - fs.borrow_mut() - .remove_directory_php(&string_arg(0)?) - .map_err(failed)?, - )), + "removeDirectoryPhp" => Ok(fs + .borrow_mut() + .remove_directory_php(&string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), "ensureDirectoryExists" => { fs.borrow_mut() .ensure_directory_exists(&string_arg(0)?) .map_err(failed)?; Ok(PluginValue::Null) } - "unlink" => Ok(PluginValue::Bool( - fs.borrow().unlink(string_arg(0)?).map_err(failed)?, - )), - "rmdir" => Ok(PluginValue::Bool( - fs.borrow().rmdir(string_arg(0)?).map_err(failed)?, - )), + "unlink" => Ok(fs + .borrow() + .unlink(string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), + "rmdir" => Ok(fs + .borrow() + .rmdir(string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), "copyThenRemove" => { fs.borrow_mut() .copy_then_remove(&string_arg(0)?, &string_arg(1)?) .map_err(failed)?; Ok(PluginValue::Null) } - "copy" => Ok(PluginValue::Bool( - fs.borrow_mut() - .copy(&string_arg(0)?, &string_arg(1)?) - .map_err(failed)?, - )), + "copy" => Ok(fs + .borrow_mut() + .copy(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)? + .to_plugin_value()), "rename" => { fs.borrow_mut() .rename(string_arg(0)?, string_arg(1)?) @@ -1022,56 +924,62 @@ fn dispatch_filesystem_method( code: 0, }); } - Ok(PluginValue::string(if method_name == "findShortestPath" { + Ok(if method_name == "findShortestPath" { fs.find_shortest_path( &from, &to, - bool_arg(method_name, args.get(2))?, - bool_arg(method_name, args.get(3))?, + arg::<bool>(method_name, args, 2)?, + arg::<bool>(method_name, args, 3)?, ) } else { fs.find_shortest_path_code( &from, &to, - bool_arg(method_name, args.get(2))?, - bool_arg(method_name, args.get(3))?, - bool_arg(method_name, args.get(4))?, + arg::<bool>(method_name, args, 2)?, + arg::<bool>(method_name, args, 3)?, + arg::<bool>(method_name, args, 4)?, ) - })) + } + .to_plugin_value()) } - "isAbsolutePath" => Ok(PluginValue::Bool( - fs.borrow().is_absolute_path(&string_arg(0)?), - )), - "size" => Ok(PluginValue::Int( - fs.borrow().size(string_arg(0)?).map_err(failed)?, - )), - "normalizePath" => Ok(PluginValue::string( - fs.borrow().normalize_path(&string_arg(0)?), - )), - "relativeSymlink" => Ok(PluginValue::Bool( - fs.borrow() - .relative_symlink(&string_arg(0)?, &string_arg(1)?), - )), - "isSymlinkedDirectory" => Ok(PluginValue::Bool( - fs.borrow().is_symlinked_directory(&string_arg(0)?), - )), + "isAbsolutePath" => Ok(fs + .borrow() + .is_absolute_path(&string_arg(0)?) + .to_plugin_value()), + "size" => Ok(fs + .borrow() + .size(string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), + "normalizePath" => Ok(fs + .borrow() + .normalize_path(&string_arg(0)?) + .to_plugin_value()), + "relativeSymlink" => Ok(fs + .borrow() + .relative_symlink(&string_arg(0)?, &string_arg(1)?) + .to_plugin_value()), + "isSymlinkedDirectory" => Ok(fs + .borrow() + .is_symlinked_directory(&string_arg(0)?) + .to_plugin_value()), "junction" => { fs.borrow_mut() .junction(&string_arg(0)?, &string_arg(1)?) .map_err(failed)?; Ok(PluginValue::Null) } - "isJunction" => Ok(PluginValue::Bool(fs.borrow().is_junction(&string_arg(0)?))), - "removeJunction" => Ok(PluginValue::Bool( - fs.borrow_mut() - .remove_junction(&string_arg(0)?) - .map_err(failed)?, - )), - "filePutContentsIfModified" => Ok(PluginValue::Int( - fs.borrow() - .file_put_contents_if_modified(&string_arg(0)?, &string_arg(1)?) - .map_err(failed)?, - )), + "isJunction" => Ok(fs.borrow().is_junction(&string_arg(0)?).to_plugin_value()), + "removeJunction" => Ok(fs + .borrow_mut() + .remove_junction(&string_arg(0)?) + .map_err(failed)? + .to_plugin_value()), + "filePutContentsIfModified" => Ok(fs + .borrow() + .file_put_contents_if_modified(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)? + .to_plugin_value()), "safeCopy" => { fs.borrow() .safe_copy(&string_arg(0)?, &string_arg(1)?) @@ -1093,14 +1001,7 @@ fn dispatch_event_dispatcher_method( ) -> Result<PluginValue, PhpThrow> { match method_name { "dispatch" => { - let name = match args.first() { - Some(PluginValue::String(name)) => String::from_utf8_lossy(name).into_owned(), - other => { - return Err(runtime_throw(format!( - "dispatch expects an event name, got {other:?}" - ))); - } - }; + let name = arg::<String>(method_name, args, 0)?; let probe = crate::event_dispatcher::Event::from_name(name.clone()); if dispatcher.borrow_mut().has_event_listeners(&probe) { // TODO(plugin): dispatching a worker-constructed event through the Rust-side @@ -1140,16 +1041,16 @@ fn dispatch_repository_method( ) -> Result<PluginValue, PhpThrow> { match method_name { "hasPackage" => { - let package = package_from_arg(method_name, args.first())?; + let package = arg::<PackageInterfaceHandle>(method_name, args, 0)?; let has = repository.has_package(package).map_err(|error| { // TODO(plugin): the original exception class is collapsed to RuntimeException // on this side of the boundary. runtime_throw(format!("hasPackage failed over RPC: {error}")) })?; - Ok(PluginValue::Bool(has)) + Ok(has.to_plugin_value()) } "addPackage" | "removePackage" => { - let package = package_from_arg(method_name, args.first())?; + let package = arg::<PackageInterfaceHandle>(method_name, args, 0)?; let mut borrowed = repository.borrow_mut(); let writable = borrowed .as_writable_repository_interface_mut() @@ -1190,72 +1091,198 @@ fn dispatch_repository_method( } } -/// A PHP string-or-null wire value. -fn optional_string(value: Option<String>) -> PluginValue { - match value { - Some(value) => PluginValue::string(value), - None => PluginValue::Null, - } +/// Decodes one positional argument of an RPC method call. A missing argument decodes the same +/// way PHP's `null` does; a parameter's own default belongs to `arg_or`. +trait FromPluginArg: Sized { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow>; } -fn string_list(values: Vec<String>) -> PluginValue { - PluginValue::List(values.into_iter().map(PluginValue::string).collect()) +/// Encodes a Rust value as the PHP value the calling proxy stub declares. +trait ToPluginValue { + fn to_plugin_value(self) -> PluginValue; } -/// `?list<array{url: string, preferred: bool}>` as PHP shapes it. -fn mirror_list(mirrors: Option<Vec<crate::package::Mirror>>) -> PluginValue { - match mirrors { - None => PluginValue::Null, - Some(mirrors) => PluginValue::List( - mirrors - .into_iter() - .map(|mirror| { - PluginValue::Array(IndexMap::from([ - (b"url".to_vec(), PluginValue::string(mirror.url)), - (b"preferred".to_vec(), PluginValue::Bool(mirror.preferred)), - ])) - }) - .collect(), - ), - } +/// Decodes the argument at `position`. +fn arg<T: FromPluginArg>( + method: &str, + args: &[PluginValue], + position: usize, +) -> Result<T, PhpThrow> { + T::from_arg(method, position, args.get(position)) } -/// An `array<string, mixed>` as PHP shapes it: empty maps cross as a list, since an empty PHP -/// array is indistinguishable from an empty list on the wire. -fn string_keyed_map(map: IndexMap<String, PhpMixed>) -> PluginValue { - if map.is_empty() { - PluginValue::List(Vec::new()) - } else { - PluginValue::from_php_mixed(&PhpMixed::Array(map)) +/// Decodes the argument at `position` for a parameter whose PHP declaration carries a default: +/// an omitted argument, and the `null` PHP passes in its place, take `default`. +fn arg_or<T: FromPluginArg>( + method: &str, + args: &[PluginValue], + position: usize, + default: T, +) -> Result<T, PhpThrow> { + match args.get(position) { + None | Some(PluginValue::Null) => Ok(default), + value => T::from_arg(method, position, value), } } -/// The inverse of `mirror_list`. -fn decode_mirrors( +fn arg_throw( method: &str, + position: usize, + expected: &str, value: Option<&PluginValue>, -) -> Result<Option<Vec<crate::package::Mirror>>, PhpThrow> { - let rows = match value { - None | Some(PluginValue::Null) => return Ok(None), - Some(PluginValue::List(rows)) => rows.clone(), - Some(PluginValue::Array(rows)) => rows.values().cloned().collect(), - other => { - return Err(runtime_throw(format!( - "{method} expects a list of mirrors or null, got {other:?}" - ))); +) -> PhpThrow { + runtime_throw(format!( + "{method} expects {expected} at position {position}, got {value:?}" + )) +} + +impl FromPluginArg for bool { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::Bool(value)) => Ok(*value), + other => Err(arg_throw(method, position, "a bool", other)), } - }; - let mut mirrors = Vec::with_capacity(rows.len()); - for row in rows { - let row = match row { - PluginValue::Array(row) => row, - other => { - return Err(runtime_throw(format!( - "{method} expects mirror maps, got {other:?}" - ))); - } + } +} + +impl FromPluginArg for i64 { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::Int(value)) => Ok(*value), + other => Err(arg_throw(method, position, "an int", other)), + } + } +} + +impl FromPluginArg for String { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + // TODO(bytes): lossy UTF-8; every PHP string crossing the boundary is bytes. + Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), + other => Err(arg_throw(method, position, "a string", other)), + } + } +} + +impl FromPluginArg for PhpMixed { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + None => Ok(PhpMixed::Null), + Some(value) => value.to_php_mixed().map_err(|error| { + runtime_throw(format!( + "{method} could not decode its argument at position {position}: {error:#}" + )) + }), + } + } +} + +impl<T: FromPluginArg> FromPluginArg for Option<T> { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + None | Some(PluginValue::Null) => Ok(None), + value => Ok(Some(T::from_arg(method, position, value)?)), + } + } +} + +/// A PHP array argument as a list, accepting the keyed shape PHP allows anywhere a list is +/// documented. +impl<T: FromPluginArg> FromPluginArg for Vec<T> { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + let items: Vec<&PluginValue> = match value { + Some(PluginValue::List(items)) => items.iter().collect(), + Some(PluginValue::Array(map)) => map.values().collect(), + other => return Err(arg_throw(method, position, "an array", other)), + }; + items + .into_iter() + .map(|item| T::from_arg(method, position, Some(item))) + .collect() + } +} + +/// A PHP array argument as a map. An empty PHP array is indistinguishable from an empty list on +/// the wire, so it decodes here as an empty map; a non-empty list decodes under its int keys. +impl<T: FromPluginArg> FromPluginArg for IndexMap<String, T> { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + let entries: Vec<(String, &PluginValue)> = match value { + Some(PluginValue::Array(map)) => map + .iter() + // TODO(bytes): lossy UTF-8; PHP array keys are bytes. + .map(|(key, item)| (String::from_utf8_lossy(key).into_owned(), item)) + .collect(), + Some(PluginValue::List(items)) => items + .iter() + .enumerate() + .map(|(index, item)| (index.to_string(), item)) + .collect(), + other => return Err(arg_throw(method, position, "an array", other)), + }; + entries + .into_iter() + .map(|(key, item)| Ok((key, T::from_arg(method, position, Some(item))?))) + .collect() + } +} + +impl FromPluginArg for crate::package::Link { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(value) => link_from_wire(value), + other => Err(arg_throw(method, position, "a Link", other)), + } + } +} + +impl FromPluginArg for crate::package::Mirror { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + let row = match value { + Some(PluginValue::Array(row)) => row, + other => return Err(arg_throw(method, position, "a mirror map", other)), }; let url = match row.get(b"url".as_slice()) { + // TODO(bytes): lossy UTF-8; PHP strings are bytes. Some(PluginValue::String(url)) => String::from_utf8_lossy(url).into_owned(), other => { return Err(runtime_throw(format!( @@ -1267,273 +1294,186 @@ fn decode_mirrors( row.get(b"preferred".as_slice()), Some(PluginValue::Bool(true)) ); - mirrors.push(crate::package::Mirror { url, preferred }); + Ok(crate::package::Mirror { url, preferred }) } - Ok(Some(mirrors)) } -/// An `array<string, T>` of plain values as PHP shapes it, via the `PhpMixed` image of `T`. -fn typed_map<T>(map: IndexMap<String, T>, to_mixed: impl Fn(T) -> PhpMixed) -> PluginValue { - string_keyed_map( - map.into_iter() - .map(|(key, value)| (key, to_mixed(value))) - .collect(), - ) +/// Resolves a package argument back to the Rust-side entity its proxy stub stands for. +impl FromPluginArg for PackageInterfaceHandle { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::RustHandle(handle)) => { + match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) { + Some(RustEntity::Package(package)) => { + Ok(PackageInterfaceHandle::from_rc_unchecked(package)) + } + _ => Err(runtime_throw(format!( + "{method} expects a package handle, got Rust handle {}", + handle.rhandle + ))), + } + } + other => Err(arg_throw(method, position, "a package", other)), + } + } } -/// A `list<array<string, T>>` as PHP shapes it. -fn typed_map_list<T>( - rows: Vec<IndexMap<String, T>>, - to_mixed: impl Fn(T) -> PhpMixed + Copy, -) -> PluginValue { - PluginValue::List( - rows.into_iter() - .map(|row| typed_map(row, to_mixed)) - .collect(), - ) +/// Resolves a repository argument back to the Rust-side entity its proxy stub stands for. +impl FromPluginArg for RepositoryInterfaceHandle { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::RustHandle(handle)) => { + match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) { + Some(RustEntity::Repository(repository)) => Ok(repository), + _ => Err(runtime_throw(format!( + "{method} expects a repository handle, got Rust handle {}", + handle.rhandle + ))), + } + } + other => Err(arg_throw(method, position, "a repository", other)), + } + } } -/// The `PhpMixed` image of an argument, as the wire codec decoded it. -fn mixed_arg(method: &str, value: Option<&PluginValue>) -> Result<PhpMixed, PhpThrow> { - match value { - None => Ok(PhpMixed::Null), - Some(value) => value.to_php_mixed().map_err(|error| { - runtime_throw(format!("{method} could not decode its argument: {error:#}")) - }), +impl FromPluginArg for chrono::DateTime<chrono::Utc> { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(value) => date_time_from_wire(value), + other => Err(arg_throw(method, position, "a date", other)), + } } } -/// A PHP array argument as a map. An empty PHP array is indistinguishable from an empty list on -/// the wire, so it decodes here as an empty map. -fn map_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<IndexMap<String, PhpMixed>, PhpThrow> { - match mixed_arg(method, value)? { - PhpMixed::Array(map) => Ok(map), - PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()), - PhpMixed::List(items) => Ok(items - .into_iter() - .enumerate() - .map(|(index, item)| (index.to_string(), item)) - .collect()), - other => Err(runtime_throw(format!( - "{method} expects an array, got {other:?}" - ))), +impl FromPluginArg for PhpObjHandle { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::PhpHandle(handle)) => Ok(handle.clone()), + other => Err(arg_throw(method, position, "a PHP object", other)), + } } } -/// A PHP array argument as a list, accepting the keyed shape PHP allows anywhere a list is -/// documented. -fn list_arg(method: &str, value: Option<&PluginValue>) -> Result<Vec<PhpMixed>, PhpThrow> { - match mixed_arg(method, value)? { - PhpMixed::List(items) => Ok(items), - PhpMixed::Array(map) => Ok(map.into_values().collect()), - other => Err(runtime_throw(format!( - "{method} expects an array, got {other:?}" - ))), +impl FromPluginArg for DisplayMode { + fn from_arg( + method: &str, + position: usize, + value: Option<&PluginValue>, + ) -> Result<Self, PhpThrow> { + match value { + Some(PluginValue::Int(0)) => Ok(DisplayMode::SourceRefIfDev), + Some(PluginValue::Int(1)) => Ok(DisplayMode::SourceRef), + Some(PluginValue::Int(2)) => Ok(DisplayMode::DistRef), + other => Err(arg_throw( + method, + position, + "a display mode of 0..=2", + other, + )), + } } } -fn as_string(method: &str, value: PhpMixed) -> Result<String, PhpThrow> { - match value { - PhpMixed::String(value) => Ok(value), - other => Err(runtime_throw(format!( - "{method} expects strings, got {other:?}" - ))), +impl ToPluginValue for bool { + fn to_plugin_value(self) -> PluginValue { + PluginValue::Bool(self) } } -fn as_int(method: &str, value: PhpMixed) -> Result<i64, PhpThrow> { - match value { - PhpMixed::Int(value) => Ok(value), - other => Err(runtime_throw(format!( - "{method} expects ints, got {other:?}" - ))), +impl ToPluginValue for i64 { + fn to_plugin_value(self) -> PluginValue { + PluginValue::Int(self) } } -fn string_map_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<IndexMap<String, String>, PhpThrow> { - map_arg(method, value)? - .into_iter() - .map(|(key, value)| Ok((key, as_string(method, value)?))) - .collect() +impl ToPluginValue for String { + fn to_plugin_value(self) -> PluginValue { + PluginValue::string(self) + } } -fn int_map_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<IndexMap<String, i64>, PhpThrow> { - map_arg(method, value)? - .into_iter() - .map(|(key, value)| Ok((key, as_int(method, value)?))) - .collect() +impl ToPluginValue for &str { + fn to_plugin_value(self) -> PluginValue { + PluginValue::string(self) + } } -fn string_list_arg(method: &str, value: Option<&PluginValue>) -> Result<Vec<String>, PhpThrow> { - list_arg(method, value)? - .into_iter() - .map(|item| as_string(method, item)) - .collect() +impl ToPluginValue for PhpMixed { + fn to_plugin_value(self) -> PluginValue { + PluginValue::from_php_mixed(&self) + } } -/// An `array<string, Link>` argument, keyed by the target package name as PHP keys it. -fn link_map_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<IndexMap<String, crate::package::Link>, PhpThrow> { - let entries: Vec<(Vec<u8>, &PluginValue)> = match value { - Some(PluginValue::Array(map)) => { - map.iter().map(|(key, item)| (key.clone(), item)).collect() - } - Some(PluginValue::List(items)) => items - .iter() - .enumerate() - .map(|(index, item)| (index.to_string().into_bytes(), item)) - .collect(), - None | Some(PluginValue::Null) => Vec::new(), - other => { - return Err(runtime_throw(format!( - "{method} expects an array of Link values, got {other:?}" - ))); +impl<T: ToPluginValue> ToPluginValue for Option<T> { + fn to_plugin_value(self) -> PluginValue { + match self { + Some(value) => value.to_plugin_value(), + None => PluginValue::Null, } - }; - entries - .into_iter() - .map(|(key, item)| { - Ok(( - String::from_utf8_lossy(&key).into_owned(), - link_from_wire(item)?, - )) - }) - .collect() -} - -/// A `list<array<string, string>>` argument (`authors`, `aliases`). -fn string_map_list_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<Vec<IndexMap<String, String>>, PhpThrow> { - list_arg(method, value)? - .into_iter() - .map(|row| match row { - PhpMixed::Array(row) => row - .into_iter() - .map(|(key, value)| Ok((key, as_string(method, value)?))) - .collect(), - PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()), - other => Err(runtime_throw(format!( - "{method} expects arrays of strings, got {other:?}" - ))), - }) - .collect() -} - -/// A `list<array<string, mixed>>` argument (`funding`). -fn mixed_map_list_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<Vec<IndexMap<String, PhpMixed>>, PhpThrow> { - list_arg(method, value)? - .into_iter() - .map(|row| match row { - PhpMixed::Array(row) => Ok(row), - PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()), - other => Err(runtime_throw(format!( - "{method} expects arrays, got {other:?}" - ))), - }) - .collect() -} - -/// An `array<string, list<string>>` argument (`scripts`). -fn string_list_map_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<IndexMap<String, Vec<String>>, PhpThrow> { - map_arg(method, value)? - .into_iter() - .map(|(key, value)| { - let items = match value { - PhpMixed::List(items) => items, - PhpMixed::Array(map) => map.into_values().collect(), - other => { - return Err(runtime_throw(format!( - "{method} expects arrays of strings, got {other:?}" - ))); - } - }; - Ok(( - key, - items - .into_iter() - .map(|item| as_string(method, item)) - .collect::<Result<_, _>>()?, - )) - }) - .collect() + } } -fn bool_arg(method: &str, value: Option<&PluginValue>) -> Result<bool, PhpThrow> { - match value { - Some(PluginValue::Bool(value)) => Ok(*value), - other => Err(runtime_throw(format!( - "{method} expects a bool, got {other:?}" - ))), +impl<T: ToPluginValue> ToPluginValue for Vec<T> { + fn to_plugin_value(self) -> PluginValue { + PluginValue::List(self.into_iter().map(T::to_plugin_value).collect()) } } -fn required_string_arg(method: &str, value: Option<&PluginValue>) -> Result<String, PhpThrow> { - decode_optional_string(method, value)? - .ok_or_else(|| runtime_throw(format!("{method} expects a string"))) +/// An `array<string, T>` as PHP shapes it: an empty map crosses as a list, since an empty PHP +/// array is indistinguishable from an empty list on the wire. +impl<T: ToPluginValue> ToPluginValue for IndexMap<String, T> { + fn to_plugin_value(self) -> PluginValue { + if self.is_empty() { + PluginValue::List(Vec::new()) + } else { + PluginValue::Array( + self.into_iter() + .map(|(key, value)| (key.into_bytes(), value.to_plugin_value())) + .collect(), + ) + } + } } -/// Resolves a package argument back to the Rust-side entity its proxy stub stands for. -fn package_from_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<PackageInterfaceHandle, PhpThrow> { - match value { - Some(PluginValue::RustHandle(handle)) => { - match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) { - Some(RustEntity::Package(package)) => { - Ok(PackageInterfaceHandle::from_rc_unchecked(package)) - } - _ => Err(runtime_throw(format!( - "{method} expects a package handle, got Rust handle {}", - handle.rhandle - ))), - } - } - other => Err(runtime_throw(format!( - "{method} expects a package argument, got {other:?}" - ))), +/// `array{url: string, preferred: bool}` as PHP shapes it. +impl ToPluginValue for crate::package::Mirror { + fn to_plugin_value(self) -> PluginValue { + PluginValue::Array(IndexMap::from([ + (b"url".to_vec(), PluginValue::string(self.url)), + (b"preferred".to_vec(), PluginValue::Bool(self.preferred)), + ])) } } -fn optional_package_from_arg( - method: &str, - value: Option<&PluginValue>, -) -> Result<Option<PackageInterfaceHandle>, PhpThrow> { - match value { - None | Some(PluginValue::Null) => Ok(None), - other => Ok(Some(package_from_arg(method, other)?)), +/// The child rebuilds a link as a real `Composer\Package\Link`, constraint included. +/// +/// TODO(plugin): links have no entity to intern against, so two calls of the same getter answer +/// with distinct child-side objects where upstream returns the identical one. +impl ToPluginValue for crate::package::Link { + fn to_plugin_value(self) -> PluginValue { + link_to_wire(&self) } } -fn decode_optional_string( - method: &str, - value: Option<&PluginValue>, -) -> Result<Option<String>, PhpThrow> { - match value { - None | Some(PluginValue::Null) => Ok(None), - Some(PluginValue::String(bytes)) => Ok(Some(String::from_utf8_lossy(bytes).into_owned())), - other => Err(runtime_throw(format!( - "{method} expects a string or null, got {other:?}" - ))), +impl ToPluginValue for chrono::DateTime<chrono::Utc> { + fn to_plugin_value(self) -> PluginValue { + date_time_to_wire(&self) } } @@ -1566,21 +1506,19 @@ fn dispatch_complete_package_getter( .as_complete_package_interface() .ok_or_else(unavailable)?; match method_name { - "getScripts" => typed_map(package.get_scripts(), |commands| { - PhpMixed::List(commands.into_iter().map(PhpMixed::String).collect()) - }), - "getRepositories" => string_keyed_map(package.get_repositories()), - "getLicense" => string_list(package.get_license()), - "getKeywords" => string_list(package.get_keywords()), - "getDescription" => optional_string(package.get_description()), - "getHomepage" => optional_string(package.get_homepage()), - "getAuthors" => typed_map_list(package.get_authors(), PhpMixed::String), - "getSupport" => typed_map(package.get_support(), PhpMixed::String), - "getFunding" => typed_map_list(package.get_funding(), |value| value), + "getScripts" => package.get_scripts().to_plugin_value(), + "getRepositories" => package.get_repositories().to_plugin_value(), + "getLicense" => package.get_license().to_plugin_value(), + "getKeywords" => package.get_keywords().to_plugin_value(), + "getDescription" => package.get_description().to_plugin_value(), + "getHomepage" => package.get_homepage().to_plugin_value(), + "getAuthors" => package.get_authors().to_plugin_value(), + "getSupport" => package.get_support().to_plugin_value(), + "getFunding" => package.get_funding().to_plugin_value(), "isAbandoned" => PluginValue::Bool(package.is_abandoned()), - "getReplacementPackage" => optional_string(package.get_replacement_package()), - "getArchiveName" => optional_string(package.get_archive_name()), - _ => string_list(package.get_archive_excludes()), + "getReplacementPackage" => package.get_replacement_package().to_plugin_value(), + "getArchiveName" => package.get_archive_name().to_plugin_value(), + _ => package.get_archive_excludes().to_plugin_value(), } } "getAliases" @@ -1593,12 +1531,12 @@ fn dispatch_complete_package_getter( .as_root_package_interface() .ok_or_else(unavailable)?; match method_name { - "getAliases" => typed_map_list(package.get_aliases(), PhpMixed::String), + "getAliases" => package.get_aliases().to_plugin_value(), "getMinimumStability" => PluginValue::string(package.get_minimum_stability()), - "getStabilityFlags" => typed_map(package.get_stability_flags(), PhpMixed::Int), - "getReferences" => typed_map(package.get_references(), PhpMixed::String), + "getStabilityFlags" => package.get_stability_flags().to_plugin_value(), + "getReferences" => package.get_references().to_plugin_value(), "getPreferStable" => PluginValue::Bool(package.get_prefer_stable()), - _ => string_keyed_map(package.get_config()), + _ => package.get_config().to_plugin_value(), } } _ => return Ok(None), @@ -1611,36 +1549,10 @@ fn dispatch_package_method( method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - // The link getters return `array<string, Link>`. Each link is rebuilt in the child as a - // real `Composer\Package\Link`, constraint included; an empty map crosses as a list, the - // wire image of an empty PHP array. - // - // TODO(plugin): links have no entity to intern against, so two calls of the same getter - // answer with distinct child-side objects where upstream returns the identical one. - let links = |links: IndexMap<String, crate::package::Link>| -> PluginValue { - if links.is_empty() { - PluginValue::List(Vec::new()) - } else { - PluginValue::Array( - links - .iter() - .map(|(name, link)| (name.clone().into_bytes(), link_to_wire(link))) - .collect(), - ) - } - }; - // Mutators borrow mutably and must not hold the borrow across the shared-borrow arms. match method_name { "setId" => { - let id = match args.first() { - Some(PluginValue::Int(id)) => *id, - other => { - return Err(runtime_throw(format!( - "setId expects an int, got {other:?}" - ))); - } - }; + let id = arg::<i64>(method_name, args, 0)?; package.borrow_mut().as_package_interface_mut().set_id(id); return Ok(PluginValue::Null); } @@ -1651,7 +1563,7 @@ fn dispatch_package_method( | "setDistType" | "setDistReference" | "setSourceDistReferences" => { - let value = decode_optional_string(method_name, args.first())?; + let value = arg::<Option<String>>(method_name, args, 0)?; let mut borrowed = package.borrow_mut(); let package = borrowed.as_package_interface_mut(); match method_name { @@ -1668,7 +1580,7 @@ fn dispatch_package_method( return Ok(PluginValue::Null); } "setSourceMirrors" | "setDistMirrors" => { - let mirrors = decode_mirrors(method_name, args.first())?; + let mirrors = arg::<Option<Vec<crate::package::Mirror>>>(method_name, args, 0)?; let mut borrowed = package.borrow_mut(); let package = borrowed.as_package_interface_mut(); if method_name == "setSourceMirrors" { @@ -1679,24 +1591,7 @@ fn dispatch_package_method( return Ok(PluginValue::Null); } "setRepository" => { - let repository = match args.first() { - Some(PluginValue::RustHandle(handle)) => { - match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) { - Some(RustEntity::Repository(repository)) => repository, - _ => { - return Err(runtime_throw(format!( - "setRepository expects a repository handle, got Rust handle {}", - handle.rhandle - ))); - } - } - } - other => { - return Err(runtime_throw(format!( - "setRepository expects a repository argument, got {other:?}" - ))); - } - }; + let repository = arg::<RepositoryInterfaceHandle>(method_name, args, 0)?; package .borrow_mut() .as_package_interface_mut() @@ -1705,22 +1600,8 @@ fn dispatch_package_method( return Ok(PluginValue::Null); } "setTransportOptions" => { - let options = match args.first() { - Some(value) => match value.to_php_mixed().map_err(|error| { - runtime_throw(format!( - "setTransportOptions could not decode its argument: {error:#}" - )) - })? { - PhpMixed::Array(options) => options, - PhpMixed::List(items) if items.is_empty() => IndexMap::new(), - other => { - return Err(runtime_throw(format!( - "setTransportOptions expects an array, got {other:?}" - ))); - } - }, - None => IndexMap::new(), - }; + let options = + arg_or::<IndexMap<String, PhpMixed>>(method_name, args, 0, IndexMap::new())?; package .borrow_mut() .as_package_interface_mut() @@ -1738,17 +1619,46 @@ fn dispatch_package_method( .as_root_package_interface_mut() .expect("a root package exposes RootPackageInterface"); match method_name { - "setRequires" => package.set_requires(link_map_arg(method_name, args.first())?), + "setRequires" => package.set_requires(arg_or::< + IndexMap<String, crate::package::Link>, + >( + method_name, args, 0, IndexMap::new() + )?), "setDevRequires" => { - package.set_dev_requires(link_map_arg(method_name, args.first())?) + package.set_dev_requires(arg_or::<IndexMap<String, crate::package::Link>>( + method_name, + args, + 0, + IndexMap::new(), + )?) } - "setConflicts" => package.set_conflicts(link_map_arg(method_name, args.first())?), - "setProvides" => package.set_provides(link_map_arg(method_name, args.first())?), - "setReplaces" => package.set_replaces(link_map_arg(method_name, args.first())?), - "setAutoload" => package.set_autoload(map_arg(method_name, args.first())?), - "setDevAutoload" => package.set_dev_autoload(map_arg(method_name, args.first())?), - "setSuggests" => package.set_suggests(string_map_arg(method_name, args.first())?), - _ => package.set_extra(map_arg(method_name, args.first())?), + "setConflicts" => package.set_conflicts(arg_or::< + IndexMap<String, crate::package::Link>, + >( + method_name, args, 0, IndexMap::new() + )?), + "setProvides" => package.set_provides(arg_or::< + IndexMap<String, crate::package::Link>, + >( + method_name, args, 0, IndexMap::new() + )?), + "setReplaces" => package.set_replaces(arg_or::< + IndexMap<String, crate::package::Link>, + >( + method_name, args, 0, IndexMap::new() + )?), + "setAutoload" => { + package.set_autoload(arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?) + } + "setDevAutoload" => package.set_dev_autoload(arg::<IndexMap<String, PhpMixed>>( + method_name, + args, + 0, + )?), + "setSuggests" => { + package.set_suggests(arg::<IndexMap<String, String>>(method_name, args, 0)?) + } + _ => package.set_extra(arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?), } return Ok(PluginValue::Null); } @@ -1776,42 +1686,59 @@ fn dispatch_package_method( )) })?; match method_name { - "setType" => package.set_type(required_string_arg(method_name, args.first())?), + "setType" => package.set_type(arg::<String>(method_name, args, 0)?), "setTargetDir" => { - package.set_target_dir(decode_optional_string(method_name, args.first())?) + package.set_target_dir(arg::<Option<String>>(method_name, args, 0)?) } - "setExtra" => package.set_extra(map_arg(method_name, args.first())?), - "setBinaries" => package.set_binaries(string_list_arg(method_name, args.first())?), + "setExtra" => { + package.set_extra(arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?) + } + "setBinaries" => package.set_binaries(arg::<Vec<String>>(method_name, args, 0)?), "setSourceType" => { - package.set_source_type(decode_optional_string(method_name, args.first())?) + package.set_source_type(arg::<Option<String>>(method_name, args, 0)?) + } + "setDistSha1Checksum" => { + package.set_dist_sha1_checksum(arg::<Option<String>>(method_name, args, 0)?) + } + "setSuggests" => { + package.set_suggests(arg::<IndexMap<String, String>>(method_name, args, 0)?) } - "setDistSha1Checksum" => package - .set_dist_sha1_checksum(decode_optional_string(method_name, args.first())?), - "setSuggests" => package.set_suggests(string_map_arg(method_name, args.first())?), - "setAutoload" => package.set_autoload(map_arg(method_name, args.first())?), - "setDevAutoload" => package.set_dev_autoload(map_arg(method_name, args.first())?), + "setAutoload" => { + package.set_autoload(arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?) + } + "setDevAutoload" => package.set_dev_autoload(arg::<IndexMap<String, PhpMixed>>( + method_name, + args, + 0, + )?), "setIncludePaths" => { - package.set_include_paths(string_list_arg(method_name, args.first())?) + package.set_include_paths(arg::<Vec<String>>(method_name, args, 0)?) } - "setPhpExt" => package.set_php_ext(match args.first() { - None | Some(PluginValue::Null) => None, - value => Some(map_arg(method_name, value)?), - }), + "setPhpExt" => package.set_php_ext(arg::<Option<IndexMap<String, PhpMixed>>>( + method_name, + args, + 0, + )?), "setNotificationUrl" => { - package.set_notification_url(required_string_arg(method_name, args.first())?) + package.set_notification_url(arg::<String>(method_name, args, 0)?) } "setIsDefaultBranch" => { - package.set_is_default_branch(bool_arg(method_name, args.first())?) + package.set_is_default_branch(arg::<bool>(method_name, args, 0)?) } _ => package.replace_version( - required_string_arg(method_name, args.first())?, - required_string_arg(method_name, args.get(1))?, + arg::<String>(method_name, args, 0)?, + arg::<String>(method_name, args, 1)?, ), } return Ok(PluginValue::Null); } "setRequires" | "setConflicts" | "setProvides" | "setReplaces" | "setDevRequires" => { - let links = link_map_arg(method_name, args.first())?; + let links = arg_or::<IndexMap<String, crate::package::Link>>( + method_name, + args, + 0, + IndexMap::new(), + )?; let mut borrowed = package.borrow_mut(); let package = borrowed.as_package_mut().ok_or_else(|| { runtime_throw(format!( @@ -1828,10 +1755,7 @@ fn dispatch_package_method( return Ok(PluginValue::Null); } "setReleaseDate" => { - let date = match args.first() { - None | Some(PluginValue::Null) => None, - Some(value) => Some(date_time_from_wire(value)?), - }; + let date = arg::<Option<chrono::DateTime<chrono::Utc>>>(method_name, args, 0)?; let mut borrowed = package.borrow_mut(); let package = borrowed.as_package_mut().ok_or_else(|| { runtime_throw( @@ -1854,27 +1778,31 @@ fn dispatch_package_method( })?; match method_name { "setScripts" => { - package.set_scripts(string_list_map_arg(method_name, args.first())?) - } - "setRepositories" => package.set_repositories(map_arg(method_name, args.first())?), - "setLicense" => package.set_license(string_list_arg(method_name, args.first())?), - "setKeywords" => package.set_keywords(string_list_arg(method_name, args.first())?), - "setDescription" => { - package.set_description(required_string_arg(method_name, args.first())?) - } - "setHomepage" => { - package.set_homepage(required_string_arg(method_name, args.first())?) + package.set_scripts(arg::<IndexMap<String, Vec<String>>>(method_name, args, 0)?) } + "setRepositories" => package.set_repositories(arg::<IndexMap<String, PhpMixed>>( + method_name, + args, + 0, + )?), + "setLicense" => package.set_license(arg::<Vec<String>>(method_name, args, 0)?), + "setKeywords" => package.set_keywords(arg::<Vec<String>>(method_name, args, 0)?), + "setDescription" => package.set_description(arg::<String>(method_name, args, 0)?), + "setHomepage" => package.set_homepage(arg::<String>(method_name, args, 0)?), "setAuthors" => { - package.set_authors(string_map_list_arg(method_name, args.first())?) + package.set_authors(arg::<Vec<IndexMap<String, String>>>(method_name, args, 0)?) } - "setSupport" => package.set_support(string_map_arg(method_name, args.first())?), - "setFunding" => package.set_funding(mixed_map_list_arg(method_name, args.first())?), - "setAbandoned" => package.set_abandoned(mixed_arg(method_name, args.first())?), - "setArchiveName" => { - package.set_archive_name(required_string_arg(method_name, args.first())?) + "setSupport" => { + package.set_support(arg::<IndexMap<String, String>>(method_name, args, 0)?) } - _ => package.set_archive_excludes(string_list_arg(method_name, args.first())?), + "setFunding" => package.set_funding(arg::<Vec<IndexMap<String, PhpMixed>>>( + method_name, + args, + 0, + )?), + "setAbandoned" => package.set_abandoned(arg::<PhpMixed>(method_name, args, 0)?), + "setArchiveName" => package.set_archive_name(arg::<String>(method_name, args, 0)?), + _ => package.set_archive_excludes(arg::<Vec<String>>(method_name, args, 0)?), } return Ok(PluginValue::Null); } @@ -1892,26 +1820,28 @@ fn dispatch_package_method( })?; match method_name { "setStabilityFlags" => { - package.set_stability_flags(int_map_arg(method_name, args.first())?) + package.set_stability_flags(arg::<IndexMap<String, i64>>(method_name, args, 0)?) } "setMinimumStability" => { - package.set_minimum_stability(required_string_arg(method_name, args.first())?) + package.set_minimum_stability(arg::<String>(method_name, args, 0)?) } - "setPreferStable" => { - package.set_prefer_stable(bool_arg(method_name, args.first())?) + "setPreferStable" => package.set_prefer_stable(arg::<bool>(method_name, args, 0)?), + "setConfig" => { + package.set_config(arg::<IndexMap<String, PhpMixed>>(method_name, args, 0)?) } - "setConfig" => package.set_config(map_arg(method_name, args.first())?), "setReferences" => { - package.set_references(string_map_arg(method_name, args.first())?) + package.set_references(arg::<IndexMap<String, String>>(method_name, args, 0)?) + } + _ => { + package.set_aliases(arg::<Vec<IndexMap<String, String>>>(method_name, args, 0)?) } - _ => package.set_aliases(string_map_list_arg(method_name, args.first())?), } return Ok(PluginValue::Null); } "equals" => { - let other = package_from_arg(method_name, args.first())?; + let other = arg::<PackageInterfaceHandle>(method_name, args, 0)?; let this = PackageInterfaceHandle::from_rc_unchecked(package.clone()); - return Ok(PluginValue::Bool(this.equals(&other))); + return Ok(this.equals(&other).to_plugin_value()); } // The subclasses narrow `getAliasOf`'s return type to their own alias target, but every // variant holds the one entity. @@ -1924,12 +1854,12 @@ fn dispatch_package_method( })?; return Ok(match method_name { "getAliasOf" => package_handle_value(alias.get_alias_of().as_rc()), - "isRootPackageAlias" => PluginValue::Bool(alias.is_root_package_alias()), - _ => PluginValue::Bool(alias.has_self_version_requires()), + "isRootPackageAlias" => alias.is_root_package_alias().to_plugin_value(), + _ => alias.has_self_version_requires().to_plugin_value(), }); } "setRootPackageAlias" => { - let value = bool_arg(method_name, args.first())?; + let value = arg::<bool>(method_name, args, 0)?; package .borrow_mut() .as_alias_package_mut() @@ -1952,118 +1882,75 @@ fn dispatch_package_method( let borrowed = package.borrow(); let package = borrowed.as_package_interface(); match method_name { - "getName" => Ok(PluginValue::string(package.get_name().to_string())), - "getPrettyName" => Ok(PluginValue::string(package.get_pretty_name().to_string())), + "getName" => Ok(package.get_name().to_string().to_plugin_value()), + "getPrettyName" => Ok(package.get_pretty_name().to_string().to_plugin_value()), "getNames" => { - let provides = match args.first() { - None => true, - Some(PluginValue::Bool(provides)) => *provides, - other => { - return Err(runtime_throw(format!( - "getNames expects a bool provides flag, got {other:?}" - ))); - } - }; - Ok(string_list(package.get_names(provides))) + let provides = arg_or::<bool>(method_name, args, 0, true)?; + Ok(package.get_names(provides).to_plugin_value()) } - "getId" => Ok(PluginValue::Int(package.get_id())), - "isDev" => Ok(PluginValue::Bool(package.is_dev())), - "getType" => Ok(PluginValue::string(package.get_type())), - "getTargetDir" => Ok(optional_string(package.get_target_dir())), - "getExtra" => Ok(string_keyed_map(package.get_extra())), - "getInstallationSource" => Ok(optional_string(package.get_installation_source())), - "getSourceType" => Ok(optional_string(package.get_source_type())), - "getSourceUrl" => Ok(optional_string(package.get_source_url())), - "getSourceUrls" => Ok(string_list(package.get_source_urls())), - "getSourceReference" => Ok(optional_string(package.get_source_reference())), - "getSourceMirrors" => Ok(mirror_list(package.get_source_mirrors())), - "getDistType" => Ok(optional_string(package.get_dist_type())), - "getDistUrl" => Ok(optional_string(package.get_dist_url())), - "getDistUrls" => Ok(string_list(package.get_dist_urls())), - "getDistReference" => Ok(optional_string(package.get_dist_reference())), - "getDistSha1Checksum" => Ok(optional_string(package.get_dist_sha1_checksum())), - "getDistMirrors" => Ok(mirror_list(package.get_dist_mirrors())), - "getVersion" => Ok(PluginValue::string(package.get_version().to_string())), - "getPrettyVersion" => Ok(PluginValue::string( - package.get_pretty_version().to_string(), - )), + "getId" => Ok(package.get_id().to_plugin_value()), + "isDev" => Ok(package.is_dev().to_plugin_value()), + "getType" => Ok(package.get_type().to_plugin_value()), + "getTargetDir" => Ok(package.get_target_dir().to_plugin_value()), + "getExtra" => Ok(package.get_extra().to_plugin_value()), + "getInstallationSource" => Ok(package.get_installation_source().to_plugin_value()), + "getSourceType" => Ok(package.get_source_type().to_plugin_value()), + "getSourceUrl" => Ok(package.get_source_url().to_plugin_value()), + "getSourceUrls" => Ok(package.get_source_urls().to_plugin_value()), + "getSourceReference" => Ok(package.get_source_reference().to_plugin_value()), + "getSourceMirrors" => Ok(package.get_source_mirrors().to_plugin_value()), + "getDistType" => Ok(package.get_dist_type().to_plugin_value()), + "getDistUrl" => Ok(package.get_dist_url().to_plugin_value()), + "getDistUrls" => Ok(package.get_dist_urls().to_plugin_value()), + "getDistReference" => Ok(package.get_dist_reference().to_plugin_value()), + "getDistSha1Checksum" => Ok(package.get_dist_sha1_checksum().to_plugin_value()), + "getDistMirrors" => Ok(package.get_dist_mirrors().to_plugin_value()), + "getVersion" => Ok(package.get_version().to_string().to_plugin_value()), + "getPrettyVersion" => Ok(package.get_pretty_version().to_string().to_plugin_value()), "getFullPrettyVersion" => { - let truncate = match args.first() { - None => true, - Some(PluginValue::Bool(truncate)) => *truncate, - other => { - return Err(runtime_throw(format!( - "getFullPrettyVersion expects a bool truncate flag, got {other:?}" - ))); - } - }; - let display_mode = match args.get(1) { - None | Some(PluginValue::Int(0)) => DisplayMode::SourceRefIfDev, - Some(PluginValue::Int(1)) => DisplayMode::SourceRef, - Some(PluginValue::Int(2)) => DisplayMode::DistRef, - other => { - return Err(runtime_throw(format!( - "getFullPrettyVersion expects a display mode of 0..=2, got {other:?}" - ))); - } - }; - Ok(PluginValue::string( - package.get_full_pretty_version(truncate, display_mode), - )) + let truncate = arg_or::<bool>(method_name, args, 0, true)?; + let display_mode = + arg_or::<DisplayMode>(method_name, args, 1, DisplayMode::SourceRefIfDev)?; + Ok(package + .get_full_pretty_version(truncate, display_mode) + .to_plugin_value()) } - "getStability" => Ok(PluginValue::string(package.get_stability().to_string())), - "getRequires" => Ok(links((*package.get_requires()).clone())), - "getConflicts" => Ok(links((*package.get_conflicts()).clone())), - "getProvides" => Ok(links((*package.get_provides()).clone())), - "getReplaces" => Ok(links((*package.get_replaces()).clone())), - "getDevRequires" => Ok(links((*package.get_dev_requires()).clone())), - "getSuggests" => { - let suggests = package.get_suggests(); - if suggests.is_empty() { - Ok(PluginValue::List(Vec::new())) - } else { - Ok(PluginValue::Array( - suggests - .into_iter() - .map(|(name, description)| { - (name.into_bytes(), PluginValue::string(description)) - }) - .collect(), - )) - } - } - "getAutoload" => Ok(string_keyed_map(package.get_autoload())), - "getDevAutoload" => Ok(string_keyed_map(package.get_dev_autoload())), - "getIncludePaths" => Ok(string_list(package.get_include_paths())), + "getStability" => Ok(package.get_stability().to_string().to_plugin_value()), + "getRequires" => Ok((*package.get_requires()).clone().to_plugin_value()), + "getConflicts" => Ok((*package.get_conflicts()).clone().to_plugin_value()), + "getProvides" => Ok((*package.get_provides()).clone().to_plugin_value()), + "getReplaces" => Ok((*package.get_replaces()).clone().to_plugin_value()), + "getDevRequires" => Ok((*package.get_dev_requires()).clone().to_plugin_value()), + "getSuggests" => Ok(package.get_suggests().to_plugin_value()), + "getAutoload" => Ok(package.get_autoload().to_plugin_value()), + "getDevAutoload" => Ok(package.get_dev_autoload().to_plugin_value()), + "getIncludePaths" => Ok(package.get_include_paths().to_plugin_value()), "getPhpExt" => Ok(match package.get_php_ext() { - Some(config) => string_keyed_map(config), + Some(config) => config.to_plugin_value(), None => PluginValue::Null, }), "getRepository" => match package.get_repository() { Some(repository) => repository_handle_value(&repository), None => Ok(PluginValue::Null), }, - "getBinaries" => Ok(string_list(package.get_binaries())), - "getUniqueName" => Ok(PluginValue::string(package.get_unique_name())), - "getNotificationUrl" => Ok(optional_string(package.get_notification_url())), - "__toString" => Ok(PluginValue::string(package.get_unique_name())), - "getPrettyString" => Ok(PluginValue::string(package.get_pretty_string())), - "isDefaultBranch" => Ok(PluginValue::Bool(package.is_default_branch())), + "getBinaries" => Ok(package.get_binaries().to_plugin_value()), + "getUniqueName" => Ok(package.get_unique_name().to_plugin_value()), + "getNotificationUrl" => Ok(package.get_notification_url().to_plugin_value()), + "__toString" => Ok(package.get_unique_name().to_plugin_value()), + "getPrettyString" => Ok(package.get_pretty_string().to_plugin_value()), + "isDefaultBranch" => Ok(package.is_default_branch().to_plugin_value()), // `BasePackage`'s concrete methods are not forwarded by `PackageInterface`, so both are // computed from the interface here, as `VersionSelector` already does for the second. - "isPlatform" => Ok(PluginValue::Bool(package.get_repository().is_some_and( - |repository| repository.is::<crate::repository::PlatformRepository>(), - ))), - "getStabilityPriority" => Ok(PluginValue::Int( - *crate::package::base_package::STABILITIES - .get(package.get_stability()) - .unwrap_or(&crate::package::base_package::STABILITY_STABLE), - )), - "getTransportOptions" => Ok(string_keyed_map(package.get_transport_options())), - "getReleaseDate" => Ok(match package.get_release_date() { - None => PluginValue::Null, - Some(date) => date_time_to_wire(&date), - }), + "isPlatform" => Ok(package + .get_repository() + .is_some_and(|repository| repository.is::<crate::repository::PlatformRepository>()) + .to_plugin_value()), + "getStabilityPriority" => Ok((*crate::package::base_package::STABILITIES + .get(package.get_stability()) + .unwrap_or(&crate::package::base_package::STABILITY_STABLE)) + .to_plugin_value()), + "getTransportOptions" => Ok(package.get_transport_options().to_plugin_value()), + "getReleaseDate" => Ok(package.get_release_date().to_plugin_value()), other => Err(runtime_throw(format!( "the package method `{other}` is not available over RPC yet" ))), @@ -2076,11 +1963,11 @@ fn dispatch_operation_method( args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { match (method_name, operation) { - ("getOperationType", _) => Ok(PluginValue::string(operation.get_operation_type())), - ("show", _) => Ok(PluginValue::string( - operation.show(bool_arg(method_name, args.first())?), - )), - ("__toString", _) => Ok(PluginValue::string(operation.to_string())), + ("getOperationType", _) => Ok(operation.get_operation_type().to_plugin_value()), + ("show", _) => Ok(operation + .show(arg::<bool>(method_name, args, 0)?) + .to_plugin_value()), + ("__toString", _) => Ok(operation.to_string().to_plugin_value()), ("getPackage", AnyOperation::Install(op)) => { Ok(package_handle_value(op.get_package().as_rc())) } @@ -2113,21 +2000,11 @@ fn dispatch_installation_manager_method( ) -> Result<PluginValue, PhpThrow> { match method_name { "getInstallPath" => { - let package = package_from_arg(method_name, args.first())?; - Ok(match im.borrow().get_install_path(package) { - Some(path) => PluginValue::string(path), - None => PluginValue::Null, - }) + let package = arg::<PackageInterfaceHandle>(method_name, args, 0)?; + Ok(im.borrow().get_install_path(package).to_plugin_value()) } "addInstaller" | "removeInstaller" => { - let handle = match args.first() { - Some(PluginValue::PhpHandle(handle)) => handle.clone(), - other => { - return Err(runtime_throw(format!( - "{method_name} expects an installer object, got {other:?}" - ))); - } - }; + let handle = arg::<PhpObjHandle>(method_name, args, 0)?; if !php_is_a(&handle, "Composer\\Installer\\InstallerInterface").map_err(|error| { runtime_throw(format!( "{method_name} could not type-check its argument: {error:#}" @@ -2174,11 +2051,11 @@ fn dispatch_io_method( } Ok(PluginValue::Null) } - "isInteractive" => Ok(PluginValue::Bool(io.borrow().is_interactive())), - "isVerbose" => Ok(PluginValue::Bool(io.borrow().is_verbose())), - "isVeryVerbose" => Ok(PluginValue::Bool(io.borrow().is_very_verbose())), - "isDebug" => Ok(PluginValue::Bool(io.borrow().is_debug())), - "isDecorated" => Ok(PluginValue::Bool(io.borrow().is_decorated())), + "isInteractive" => Ok(io.borrow().is_interactive().to_plugin_value()), + "isVerbose" => Ok(io.borrow().is_verbose().to_plugin_value()), + "isVeryVerbose" => Ok(io.borrow().is_very_verbose().to_plugin_value()), + "isDebug" => Ok(io.borrow().is_debug().to_plugin_value()), + "isDecorated" => Ok(io.borrow().is_decorated().to_plugin_value()), // TODO(plugin): the remaining IOInterface surface (ask*, authentications, ...) is // widened on demand, driven by explicit errors from real plugins. other => Err(runtime_throw(format!( @@ -2193,49 +2070,12 @@ fn decode_write_args( method_name: &str, args: &[PluginValue], ) -> Result<(Vec<String>, bool, i64), PhpThrow> { - // TODO(bytes): lossy UTF-8; IO messages are bytes in PHP. let messages = match args.first() { - Some(PluginValue::String(bytes)) => vec![String::from_utf8_lossy(bytes).into_owned()], - Some(PluginValue::List(items)) => { - let mut messages = Vec::with_capacity(items.len()); - for item in items { - match item { - PluginValue::String(bytes) => { - messages.push(String::from_utf8_lossy(bytes).into_owned()); - } - other => { - return Err(runtime_throw(format!( - "{method_name} expects string messages, got {other:?}" - ))); - } - } - } - messages - } - other => { - return Err(runtime_throw(format!( - "{method_name} expects a string or list of strings, got {other:?}" - ))); - } - }; - let newline = match args.get(1) { - Some(PluginValue::Bool(b)) => *b, - None => true, - other => { - return Err(runtime_throw(format!( - "{method_name} expects a bool newline flag, got {other:?}" - ))); - } - }; - let verbosity = match args.get(2) { - Some(PluginValue::Int(v)) => *v, - None => crate::io::NORMAL, - other => { - return Err(runtime_throw(format!( - "{method_name} expects an int verbosity, got {other:?}" - ))); - } + Some(PluginValue::String(_)) => vec![arg::<String>(method_name, args, 0)?], + _ => arg::<Vec<String>>(method_name, args, 0)?, }; + let newline = arg_or::<bool>(method_name, args, 1, true)?; + let verbosity = arg_or::<i64>(method_name, args, 2, crate::io::NORMAL)?; Ok((messages, newline, verbosity)) } |
