use crate::ClientCommandSender;
use crate::runtime::spawn_task;
use async_trait::async_trait;
use futures::future::BoxFuture;
use log::error;
use shvrpc::rpcdiscovery::{DirParam, LsParam};
use shvrpc::rpcframe::RpcFrame;
use shvrpc::util::{children_on_path, find_longest_path_prefix};
use shvrpc::{metamethod, RpcMessage, RpcMessageMetaTags};
use shvproto::rpcvalue;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::format;
use std::sync::Arc;
pub use shvrpc::metamethod::{AccessLevel, Flags, MetaMethod};
pub use shvrpc::rpcmessage::{RpcError, RpcErrorCode};
pub use shvproto::{RpcValue, Value};
fn builtin_dir<'a>(methods: impl IntoIterator<Item = &'a MetaMethod>, param: impl Into<DirParam>) -> RpcValue {
match param.into() {
DirParam::Brief => {
methods
.into_iter()
.map(|m| m.to_rpcvalue(metamethod::DirFormat::IMap))
.collect::<Vec<_>>()
.into()
}
DirParam::Full => {
methods
.into_iter()
.map(|m| m.to_rpcvalue(metamethod::DirFormat::Map))
.collect::<Vec<_>>()
.into()
}
DirParam::Exists(ref method_name) => {
methods
.into_iter()
.find(|m| m.name.as_ref() == method_name)
.map_or_else(|| false.into(), |m| m.to_rpcvalue(metamethod::DirFormat::IMap))
}
}
}
pub type RequestResult = Result<RpcValue, RpcError>;
pub(crate) fn process_local_dir_ls<V>(
mounts: &BTreeMap<String, V>,
frame: &RpcFrame,
) -> Option<RequestResult> {
if frame.access_level().is_none() {
return Some(Err(RpcError::new(RpcErrorCode::InvalidRequest, "Undefined access level")));
}
let method = frame.method().unwrap_or_default();
if !(method == METH_DIR || method == METH_LS) {
return None;
}
let shv_path = frame.shv_path().unwrap_or_default();
let mount = find_longest_path_prefix(mounts, shv_path);
let is_mount_point = mount.is_some();
let children_on_path = children_on_path(mounts, shv_path);
let is_leaf = children_on_path.as_ref().map_or(is_mount_point, Vec::is_empty);
if children_on_path.is_none() && !is_mount_point {
return Some(RequestResult::Err(RpcError::new(
RpcErrorCode::MethodNotFound,
format!("Invalid shv path: {shv_path}"),
)));
}
let is_in_tree = children_on_path.is_some();
let is_direct_mountpoint = mount.is_some_and(|(_, rest)| rest.is_empty());
if method == METH_DIR && is_in_tree && !is_direct_mountpoint {
if let Ok(rpcmsg) = frame.to_rpcmesage() {
let dir = builtin_dir(DIR_LS_METHODS, rpcmsg.param());
return Some(RequestResult::Ok(dir));
}
return Some(RequestResult::Err(RpcError::new(
RpcErrorCode::InvalidRequest,
"Cannot convert RPC frame to RPC message".to_string(),
)));
}
if method == METH_LS && !is_leaf {
if let Ok(rpcmsg) = frame.to_rpcmesage() {
let ls = ls_children_to_result(children_on_path, rpcmsg.param());
return Some(ls);
}
return Some(RequestResult::Err(RpcError::new(
RpcErrorCode::InvalidRequest,
"Cannot convert RPC frame to RPC message".to_string(),
)));
}
None
}
fn ls_children_to_result(children: Option<Vec<String>>, param: impl Into<LsParam>) -> RequestResult {
children.map_or_else(|| RequestResult::Err(RpcError::new(
RpcErrorCode::MethodCallException,
"Invalid shv path",
)), |dirs| match param.into() {
LsParam::List => {
let res: rpcvalue::List = dirs.iter().map(RpcValue::from).collect();
RequestResult::Ok(res.into())
},
LsParam::Exists(path) =>
RequestResult::Ok(dirs.contains(&path).into()),
})
}
#[async_trait]
pub trait StaticNode: Send + Sync + 'static {
fn methods(&self) -> &'static [MetaMethod];
async fn process_request(&self, request: RpcMessage, client_cmd_tx: ClientCommandSender) -> Option<RequestResult>;
}
#[derive(Clone)]
pub struct StaticNodeHandler(Arc<dyn StaticNode>);
#[derive(Clone)]
pub struct DynamicNodeHandler(pub(crate) Arc<dyn Fn(RpcMessage, ClientCommandSender) -> BoxFuture<'static, RequestHandlerResult> + Send + Sync>);
pub(crate) struct MethodHandler(pub(crate) Box<dyn FnOnce() -> BoxFuture<'static, Option<MethodHandlerResult<RpcValue>>> + Send>);
pub(crate) struct LsHandler(pub(crate) Box<dyn FnOnce() -> BoxFuture<'static, Option<LsHandlerResult>> + Send>);
enum MethodHandlerType {
Dir,
Ls(LsHandler),
Method(MethodHandler),
}
pub struct ResolvedRequest {
methods: MetaMethods,
handler: MethodHandlerType,
}
pub enum Method {
Dir(DirMethodResolver),
Ls(LsMethodResolver),
Other(MethodResolver),
}
impl Method {
pub fn from_request(request: &RpcMessage) -> Self {
match request.method().unwrap_or_default() {
METH_DIR => Method::Dir(DirMethodResolver(Priv)),
METH_LS => Method::Ls(LsMethodResolver(Priv)),
method => Method::Other(MethodResolver(method.into())),
}
}
}
struct Priv;
pub struct DirMethodResolver(Priv);
pub struct LsMethodResolver(Priv);
pub struct MethodResolver(String);
impl DirMethodResolver {
#[expect(clippy::unnecessary_wraps, reason = "Better ergonomics")]
pub fn resolve(&self, methods: impl Into<MetaMethods>) -> RequestHandlerResult {
Ok(ResolvedRequest {
methods: methods.into(),
handler: MethodHandlerType::Dir
})
}
}
impl LsMethodResolver {
#[expect(clippy::unnecessary_wraps, reason = "Better ergonomics")]
pub fn resolve<F, Fut>(&self, methods: impl Into<MetaMethods>, handler: F) -> RequestHandlerResult
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = LsHandlerResult> + Send + 'static,
{
Ok(ResolvedRequest {
methods: methods.into(),
handler: MethodHandlerType::Ls(LsHandler::new(async move || Some(handler().await))),
})
}
#[expect(clippy::unnecessary_wraps, reason = "Better ergonomics")]
pub fn resolve_opt<F, Fut>(&self, methods: impl Into<MetaMethods>, handler: F) -> RequestHandlerResult
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Option<LsHandlerResult>> + Send + 'static,
{
Ok(ResolvedRequest {
methods: methods.into(),
handler: MethodHandlerType::Ls(LsHandler::new(handler)),
})
}
}
impl MethodResolver {
#[expect(clippy::unnecessary_wraps, reason = "Better ergonomics")]
pub fn resolve<F, Fut, T>(&self, methods: impl Into<MetaMethods>, handler: F) -> RequestHandlerResult
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = MethodHandlerResult<T>> + Send + 'static,
T: Into<RpcValue>,
{
Ok(ResolvedRequest {
methods: methods.into(),
handler: MethodHandlerType::Method(MethodHandler::new(async move || Some(handler().await))),
})
}
#[expect(clippy::unnecessary_wraps, reason = "Better ergonomics")]
pub fn resolve_opt<F, Fut, T>(&self, methods: impl Into<MetaMethods>, handler: F) -> RequestHandlerResult
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Option<MethodHandlerResult<T>>> + Send + 'static,
T: Into<RpcValue>,
{
Ok(ResolvedRequest {
methods: methods.into(),
handler: MethodHandlerType::Method(MethodHandler::new(handler)),
})
}
pub fn method(&self) -> &str {
self.0.as_str()
}
}
pub struct UnresolvedRequest;
impl UnresolvedRequest {
pub fn to_rpcerror(&self, rq: &RpcMessage) -> RpcError {
rpc_error_unknown_method_on_path(
rq.shv_path().unwrap_or_default(),
rq.method().unwrap_or_default()
)
}
}
pub fn err_unresolved_request() -> RequestHandlerResult {
Err(UnresolvedRequest)
}
pub type RequestHandlerResult = Result<ResolvedRequest, UnresolvedRequest>;
pub type MethodHandlerResult<T> = Result<T, RpcError>;
pub type LsHandlerResult = MethodHandlerResult<Vec<String>>;
pub type MetaMethods = Cow<'static, [MetaMethod]>;
pub enum ClientNode {
Static(StaticNodeHandler),
Dynamic(DynamicNodeHandler),
}
#[async_trait]
trait NodeHandler {
async fn process_request(
&self,
request: &RpcMessage,
mount_path: &str,
client_cmd_tx: &ClientCommandSender,
) -> Option<Result<RpcValue, RpcError>>;
}
#[async_trait]
impl NodeHandler for StaticNodeHandler {
async fn process_request(
&self,
request: &RpcMessage,
mount_path: &str,
client_cmd_tx: &ClientCommandSender,
) -> Option<Result<RpcValue, RpcError>>
{
let Self(node) = self;
let is_path_of_this_node = request.shv_path().unwrap_or_default().is_empty();
let methods = if is_path_of_this_node {
DIR_LS_METHODS.iter().chain(node.methods()).collect()
} else {
Cow::from(&[])
};
let granted_method = match check_request_access(
request,
mount_path,
methods.iter().copied())
{
Ok(method) => method,
Err(err) => return Some(Err(err)),
};
match granted_method {
crate::clientnode::METH_DIR =>
Some(Ok(builtin_dir(methods.iter().copied(), request.param()))),
crate::clientnode::METH_LS =>
Some(Ok(builtin_ls(request.param()))),
_ =>
node.process_request(request.clone(), client_cmd_tx.clone()).await,
}
}
}
impl DynamicNodeHandler {
pub fn new<F, Fut>(func: F) -> Self
where
F: Fn(RpcMessage, ClientCommandSender) -> Fut + Sync + Send + 'static,
Fut: Future<Output = RequestHandlerResult> + Send + 'static
{
Self(Arc::new(move |rq, tx| Box::pin(func(rq, tx))))
}
}
#[async_trait]
impl NodeHandler for DynamicNodeHandler {
async fn process_request(
&self,
request: &RpcMessage,
mount_path: &str,
client_cmd_tx: &ClientCommandSender,
) -> Option<Result<RpcValue, RpcError>>
{
let Self(request_handler) = self;
match request_handler(request.clone(), client_cmd_tx.clone()).await {
Ok(ResolvedRequest { methods, handler }) => {
fn get_method<'a>(methods: &'a MetaMethods, name: &str) -> Option<(usize, &'a MetaMethod)> {
methods
.iter()
.enumerate()
.find(|(_, mm)| mm.name == name)
}
fn extract_second_field<A, B>(tuple: (A, B)) -> B { tuple.1 }
match handler {
MethodHandlerType::Dir => {
let all_methods = |methods: MetaMethods| if methods.is_empty() {
Cow::Borrowed(DIR_LS_METHODS)
} else {
let dir_ls_methods = match (get_method(&methods, METH_DIR), get_method(&methods, METH_LS)) {
(None, None) => [META_METHOD_DIR, META_METHOD_LS],
(None, Some((_, mm_ls))) => [META_METHOD_DIR, mm_ls.to_owned()],
(Some((_, mm_dir)), None) => [mm_dir.to_owned(), META_METHOD_LS],
(Some((0, _mm_dir)), Some((1, _mm_ls))) => {
return methods
}
(Some((_, mm_dir)), Some((_, mm_ls))) => [mm_dir.to_owned(), mm_ls.to_owned()],
};
dir_ls_methods
.into_iter()
.chain(methods
.into_owned()
.into_iter()
.filter(|mm| mm.name != METH_DIR && mm.name != METH_LS)
)
.collect()
};
let mm_dir = get_method(&methods, METH_DIR)
.map_or(static_ref::META_METHOD_DIR, extract_second_field);
let response = check_request_access_for_method(request, mount_path, mm_dir)
.map(|()| builtin_dir(all_methods(methods).as_ref(), request.param()));
Some(response)
}
MethodHandlerType::Ls(LsHandler(ls_handler)) => {
let mm_ls = get_method(&methods, METH_LS)
.map_or(static_ref::META_METHOD_LS, extract_second_field);
if let Err(err) = check_request_access_for_method(request, mount_path, mm_ls) {
return Some(Err(err));
}
ls_handler()
.await
.map(|ls_result| {
let children = ls_result?;
ls_children_to_result(Some(children), request.param())
})
},
MethodHandlerType::Method(MethodHandler(method_handler)) => {
let method = request.method().unwrap_or_default();
let Some((_, mm)) = get_method(&methods, method) else {
let err = rpc_error_unknown_method_on_path(
full_shv_path(mount_path, request.shv_path().unwrap_or_default()),
method
);
return Some(Err(err));
};
if let Err(err) = check_request_access_for_method(request, mount_path, mm) {
return Some(Err(err));
}
method_handler().await
}
}
}
Err(err) => Some(Err(err.to_rpcerror(request))),
}
}
}
impl MethodHandler {
pub fn new<F, Fut, T>(func: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Option<MethodHandlerResult<T>>> + Send + 'static,
T: Into<RpcValue>,
{
Self(Box::new(move || Box::pin(async move {
func().await.map(|res| res.map(Into::into))
})))
}
}
impl LsHandler {
pub fn new<F, Fut>(func: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Option<LsHandlerResult>> + Send + 'static
{
Self(Box::new(move || Box::pin(func())))
}
}
impl ClientNode {
pub fn new_static(node: impl StaticNode) -> Self {
Self::Static(StaticNodeHandler(Arc::new(node)))
}
pub fn new_dynamic<F, Fut>(func: F) -> Self
where
F: Fn(RpcMessage, ClientCommandSender) -> Fut + Sync + Send + 'static,
Fut: Future<Output = RequestHandlerResult> + Send + 'static
{
Self::Dynamic(DynamicNodeHandler::new(func))
}
pub(crate) fn process_request(&self, request: RpcMessage, mount_path: String, client_cmd_tx: ClientCommandSender) {
fn spawn_task_for_handler(
handler: impl NodeHandler + Send + 'static,
request: RpcMessage,
mount_path: String,
client_cmd_tx: ClientCommandSender
) {
spawn_task(async move {
if let Some(result) = handler.process_request(&request, &mount_path, &client_cmd_tx).await {
send_response(&request, &client_cmd_tx, result);
}
}).detach();
}
match &self {
Self::Static(node_handler) =>
spawn_task_for_handler(node_handler.clone(), request, mount_path, client_cmd_tx),
Self::Dynamic(node_handler) =>
spawn_task_for_handler(node_handler.clone(), request, mount_path, client_cmd_tx),
}
}
}
pub fn rpc_error_unknown_method_on_path(path: impl Display, method: impl Display) -> RpcError {
RpcError::new(
RpcErrorCode::MethodNotFound,
format!("Unknown method on path '{path}:{method}()'")
)
}
pub fn full_shv_path<'a>(mount_path: impl Into<Cow<'a, str>> + Display, shv_path: impl Into<Cow<'a, str>>) -> String {
let shv_path = shv_path.into();
if shv_path.is_empty() {
mount_path.into().to_string()
} else {
format!("{mount_path}/{shv_path}")
}
}
fn check_request_access_for_method(rq: &RpcMessage, mount_path: impl AsRef<str>, method: &MetaMethod) -> Result<(), RpcError> {
let rq_level = rq.access_level().unwrap_or_default();
if rq_level >= method.access as i32 {
Ok(())
} else {
let path = full_shv_path(mount_path.as_ref(), rq.shv_path().unwrap_or_default());
Err(rpc_error_unknown_method_on_path(path, &method.name))
}
}
fn check_request_access<'a, 'r>(
request: &'r RpcMessage,
mount_path: impl AsRef<str>,
methods: impl IntoIterator<Item = &'a MetaMethod>
) -> Result<&'r str, RpcError> {
let method = request.method().unwrap_or_default();
let Some(mm) = methods.into_iter().find(|mm| mm.name == method) else {
let path = full_shv_path(mount_path.as_ref(), request.shv_path().unwrap_or_default());
return Err(rpc_error_unknown_method_on_path(path, method))
};
check_request_access_for_method(request, mount_path, mm).map(|()| method)
}
pub fn send_response(request: &RpcMessage, client_cmd_tx: &ClientCommandSender, result: Result<RpcValue, RpcError>) {
match request.prepare_response() {
Err(err) => {
error!("Cannot prepare response. Error: {err}, request: {request}");
}
Ok(mut resp) => {
match result {
Ok(result) => resp.set_result(result),
Err(err) => resp.set_error(err),
};
if let Err(e) = client_cmd_tx.send_message(resp) {
error!("Cannot send response. Error: {e}, request: {request}");
}
}
}
}
pub fn builtin_ls(rq_param: Option<&RpcValue>) -> RpcValue {
match LsParam::from(rq_param) {
LsParam::List => rpcvalue::List::new().into(),
LsParam::Exists(_path) => false.into(),
}
}
pub const METH_DIR: &str = "dir";
pub const METH_LS: &str = "ls";
pub const METH_GET: &str = "get";
pub const METH_SET: &str = "set";
pub const SIG_CHNG: &str = "chng";
pub const METH_PING: &str = "ping";
pub(crate) const DIR_LS_METHODS: &[MetaMethod] = &[META_METHOD_DIR, META_METHOD_LS];
pub mod static_ref {
use shvrpc::metamethod::MetaMethod;
pub static META_METHOD_DIR: &MetaMethod = &super::META_METHOD_DIR;
pub static META_METHOD_LS: &MetaMethod = &super::META_METHOD_LS;
}
pub const META_METHOD_DIR: MetaMethod = MetaMethod::new_static(
METH_DIR,
Flags::None,
AccessLevel::Browse,
"DirParam",
"DirResult",
&[],
"",
);
pub const META_METHOD_LS: MetaMethod = MetaMethod::new_static(
METH_LS,
Flags::None,
AccessLevel::Browse,
"LsParam",
"LsResult",
&[],
"",
);
pub const META_METHOD_GET: MetaMethod = MetaMethod::new_static(
METH_GET,
Flags::IsGetter,
AccessLevel::Read,
"",
"",
&[],
"",
);
pub const META_METHOD_SET: MetaMethod = MetaMethod::new_static(
METH_SET,
Flags::IsSetter,
AccessLevel::Write,
"",
"",
&[],
"",
);
pub const PROPERTY_METHODS: &[MetaMethod] = &[
META_METHOD_GET,
META_METHOD_SET,
];
#[cfg(test)]
mod tests {
use shvrpc::rpcdiscovery::{DirResult, MethodInfo};
use super::*;
#[test]
fn process_request_static_node() {
let node = crate::static_node!{
TestStaticNode(request, _tx) {
"echo" [IsGetter, Read, "Int", "Int"] (param: i32) => {
Some(Ok(param.into()))
}
"unhandled" [IsGetter | IsSetter, Write, "", ""] => {
None
}
}
};
let methods = node.methods();
assert_eq!(methods.len(), 2, "Expected 2 methods");
use shvrpc::metamethod::{Flags, AccessLevel};
let method_echo = &methods[0];
assert_eq!(method_echo.name, "echo");
assert_eq!(method_echo.flags, Flags::IsGetter);
assert_eq!(method_echo.access, AccessLevel::Read);
assert_eq!(method_echo.param, "Int");
assert_eq!(method_echo.result, "Int");
let method_unhandled = &methods[1];
assert_eq!(method_unhandled.name, "unhandled");
assert_eq!(method_unhandled.flags, Flags::IsGetter | Flags::IsSetter);
assert_eq!(method_unhandled.access, AccessLevel::Write);
assert_eq!(method_unhandled.param, "");
assert_eq!(method_unhandled.result, "");
crate::runtime::block_on(async move {
let node_handler = StaticNodeHandler(Arc::new(node));
let (ccs, _ccr) = futures::channel::mpsc::unbounded();
let ccs = ClientCommandSender::from_raw(ccs);
let make_request = |shvpath, method, param: Option<RpcValue>, access_level| {
let mut rq = RpcMessage::new_request(shvpath, method);
if let Some(param) = param {
rq.set_param(param);
}
rq.set_access_level(access_level);
rq
};
let res = node_handler.process_request(
&make_request(
"subpath",
"echo",
Some(32.into()),
AccessLevel::Read,
),
"mount/path",
&ccs,
).await;
let res = res.unwrap_or_else(|| panic!("Result should be Some"));
assert!(res.is_err_and(|err| err.code == RpcErrorCode::MethodNotFound.into()));
let res = node_handler.process_request(
&make_request(
"",
"echo",
Some("blah".into()),
AccessLevel::Browse,
),
"",
&ccs,
).await;
let res = res.unwrap_or_else(|| panic!("Result should be Some"));
let Err(err) = res else {
panic!("Result should be an error");
};
assert_eq!(err.code, RpcErrorCode::MethodNotFound.into());
let res = node_handler.process_request(
&make_request(
"",
"echo",
Some("blah".into()),
AccessLevel::Read,
),
"",
&ccs,
).await;
let res = res.unwrap_or_else(|| panic!("Result should be Some"));
let Err(err) = res else {
panic!("Result should be an error");
};
assert_eq!(err.code, RpcErrorCode::InvalidParam.into());
let res = node_handler.process_request(
&make_request(
"",
"echo",
Some(42.into()),
AccessLevel::Read,
),
"",
&ccs,
).await;
let res = res.unwrap_or_else(|| panic!("Result should be Some"));
assert!(res.is_ok_and(|val| val == 42.into()));
let res = node_handler.process_request(
&make_request(
"",
"dir",
Some("echo".into()),
AccessLevel::Browse,
),
"",
&ccs,
).await;
let res: bool = res
.unwrap_or_else(|| panic!("Result should be Some"))
.map_or_else(|_| panic!("Result should be Ok"), DirResult::try_from)
.unwrap_or_else(|_| panic!("Result should be a DirResult"))
.try_into()
.unwrap_or_else(|_| panic!("Result should be a bool"));
assert!(res);
let res = node_handler.process_request(
&make_request(
"",
"dir",
None,
AccessLevel::Browse,
),
"",
&ccs,
).await;
let res: Vec<MethodInfo> = res
.unwrap_or_else(|| panic!("Result should be Some"))
.map_or_else(|_| panic!("Result should be Ok"), DirResult::try_from)
.unwrap_or_else(|_| panic!("Result should be a DirResult"))
.try_into()
.unwrap_or_else(|_| panic!("Result should be a list of methods"));
assert_eq!(res.len(), 4);
assert_eq!(res[0].name, METH_DIR);
assert_eq!(res[1].name, METH_LS);
assert_eq!(res[2].name, "echo");
assert_eq!(res[3].name, "unhandled");
let res = node_handler.process_request(
&make_request(
"",
"unhandled",
None,
AccessLevel::Config,
),
"",
&ccs,
).await;
assert!(res.is_none());
});
}
#[test]
fn longest_path_prefix() {
#[expect(clippy::zero_sized_map_values, reason = "Fine for tests")]
let map = BTreeMap::from([
("".to_string(), ()),
("foo".to_string(), ()),
("foo/bar".to_string(), ()),
("x".to_string(), ())
]);
assert_eq!(find_longest_path_prefix(&map, ""), Some(("", "")));
assert_eq!(find_longest_path_prefix(&map, "blah"), Some(("", "blah")));
assert_eq!(find_longest_path_prefix(&map, "foo/blah"), Some(("foo", "blah")));
assert_eq!(find_longest_path_prefix(&map, "/"), Some(("", "")));
}
fn make_request_frame(path: &str, method: &str, param: Option<RpcValue>) -> RpcFrame {
let mut rq = RpcMessage::new_request(path, method);
if let Some(param) = param {
rq.set_param(param);
}
rq.set_access_level(AccessLevel::Read);
rq.to_frame().unwrap()
}
#[test]
fn local_dir_ls_with_root() {
#[expect(clippy::zero_sized_map_values, reason = "Fine for tests")]
let mounts = BTreeMap::from([
("".to_string(), ()),
("foo/x".to_string(), ()),
("foo/bar".to_string(), ()),
("x".to_string(), ())
]);
assert!(process_local_dir_ls(&mounts, &make_request_frame("", METH_DIR, None)).is_none());
assert!(
process_local_dir_ls(&mounts, &make_request_frame("blah", METH_DIR, None))
.is_none()
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("foo", METH_DIR, None))
.is_some_and(|res| {
let RequestResult::Ok(resp) = res else {
panic!("Not a response");
};
builtin_dir(DIR_LS_METHODS, DirParam::Brief) == resp
})
);
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x", METH_DIR, None)).is_none());
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x/y", METH_DIR, None)).is_none());
assert!(
process_local_dir_ls(&mounts, &make_request_frame("", METH_LS, None))
.is_some_and(|res| {
matches!((ls_children_to_result(Some(vec!["foo".into(), "x".into()]), LsParam::List), res), (Ok(a), Ok(b)) if a == b)
})
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("blah", METH_LS, None))
.is_none()
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("foo", METH_LS, None))
.is_some_and(|res| {
matches!((ls_children_to_result(Some(vec!["bar".into(), "x".into()]), LsParam::List), res), (Ok(a), Ok(b)) if a == b)
})
);
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x", METH_LS, None)).is_none());
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x/y", METH_LS, None)).is_none());
}
#[test]
fn local_dir_ls_without_root() {
#[expect(clippy::zero_sized_map_values, reason = "Fine for tests")]
let mounts = BTreeMap::from([
("foo".to_string(), ()),
("foo/x/y".to_string(), ()),
("foo/bar".to_string(), ()),
("z".to_string(), ())
]);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("", METH_DIR, None))
.is_some_and(|res| {
let Ok(resp) = res else {
panic!("Not a response");
};
builtin_dir(DIR_LS_METHODS, DirParam::Brief) == resp
})
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("blah", METH_DIR, None))
.is_some_and(|resp| resp.is_err())
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("foo", METH_DIR, None)).is_none());
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x", METH_DIR, None))
.is_some_and(|res| {
let Ok(resp) = res else {
panic!("Not a response");
};
builtin_dir(DIR_LS_METHODS, DirParam::Brief) == resp
})
);
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x/y", METH_DIR, None)).is_none());
assert!(
process_local_dir_ls(&mounts, &make_request_frame("", METH_LS, None))
.is_some_and(|res| {
matches!((ls_children_to_result(Some(vec!["foo".into(), "z".into()]), LsParam::List), res), (Ok(a), Ok(b)) if a == b)
})
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("blah", METH_LS, None))
.is_some_and(|resp| resp.is_err())
);
assert!(
process_local_dir_ls(&mounts, &make_request_frame("foo", METH_LS, None))
.is_some_and(|res| {
matches!((ls_children_to_result(Some(vec!["bar".into(), "x".into()]), LsParam::List), res), (Ok(a), Ok(b)) if a == b)
})
);
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x", METH_LS, None))
.is_some_and(|res| {
matches!((ls_children_to_result(Some(vec!["y".into()]), LsParam::List), res), (Ok(a), Ok(b)) if a == b)
})
);
assert!(process_local_dir_ls(&mounts, &make_request_frame("foo/x/y", METH_LS, None)).is_none());
}
}