diff --git a/Cargo.lock b/Cargo.lock index 03aec70..5bbfa56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,12 @@ dependencies = [ [[package]] name = "distant-core-plugin" version = "0.21.0" +dependencies = [ + "async-trait", + "distant-core-auth", + "distant-core-protocol", + "serde", +] [[package]] name = "distant-core-protocol" diff --git a/distant-core-plugin/Cargo.toml b/distant-core-plugin/Cargo.toml index 75f4cfe..f4a7159 100644 --- a/distant-core-plugin/Cargo.toml +++ b/distant-core-plugin/Cargo.toml @@ -10,3 +10,9 @@ homepage = "https://github.com/chipsenkbeil/distant" repository = "https://github.com/chipsenkbeil/distant" readme = "README.md" license = "MIT OR Apache-2.0" + +[dependencies] +async-trait = "0.1.68" +distant-core-auth = { version = "=0.21.0", path = "../distant-core-auth" } +distant-core-protocol = { version = "=0.21.0", path = "../distant-core-protocol" } +serde = { version = "1.0.163", features = ["derive"] } diff --git a/distant-core-plugin/src/api.rs b/distant-core-plugin/src/api.rs new file mode 100644 index 0000000..5b2b68c --- /dev/null +++ b/distant-core-plugin/src/api.rs @@ -0,0 +1,29 @@ +/// Full API that represents a distant-compatible server. +pub trait Api { + type FileSystem: FileSystemApi; + type Process: ProcessApi; + type Search: SearchApi; + type SystemInfo: SystemInfoApi; + type Version: VersionApi; +} + +/// API supporting filesystem operations. +pub trait FileSystemApi {} + +/// API supporting process creation and manipulation. +pub trait ProcessApi {} + +/// API supporting searching through the remote system. +pub trait SearchApi {} + +/// API supporting retrieval of information about the remote system. +pub trait SystemInfoApi {} + +/// API supporting retrieval of the server's version. +pub trait VersionApi {} + +/// Generic struct that implements all APIs as unsupported. +pub struct Unsupported; + +impl FileSystemApi for Unsupported { +} diff --git a/distant-core-plugin/src/common.rs b/distant-core-plugin/src/common.rs new file mode 100644 index 0000000..5f8f628 --- /dev/null +++ b/distant-core-plugin/src/common.rs @@ -0,0 +1,6 @@ +mod destination; +mod map; +mod utils; + +pub use destination::{Destination, Host, HostParseError}; +pub use map::{Map, MapParseError}; diff --git a/distant-core-net/src/common/destination.rs b/distant-core-plugin/src/common/destination.rs similarity index 100% rename from distant-core-net/src/common/destination.rs rename to distant-core-plugin/src/common/destination.rs diff --git a/distant-core-net/src/common/destination/host.rs b/distant-core-plugin/src/common/destination/host.rs similarity index 92% rename from distant-core-net/src/common/destination/host.rs rename to distant-core-plugin/src/common/destination/host.rs index 797aab7..b0a539e 100644 --- a/distant-core-net/src/common/destination/host.rs +++ b/distant-core-plugin/src/common/destination/host.rs @@ -2,7 +2,6 @@ use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::str::FromStr; -use derive_more::{Display, Error, From}; use serde::de::Deserializer; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; @@ -10,7 +9,7 @@ use serde::{Deserialize, Serialize}; use super::{deserialize_from_str, serialize_to_str}; /// Represents the host of a destination -#[derive(Clone, Debug, From, Display, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Host { Ipv4(Ipv4Addr), Ipv6(Ipv6Addr), @@ -69,7 +68,41 @@ impl From for Host { } } -#[derive(Copy, Clone, Debug, Error, Hash, PartialEq, Eq)] +impl From for Host { + fn from(addr: Ipv4Addr) -> Self { + Self::Ipv4(addr) + } +} + +impl From for Host { + fn from(addr: Ipv6Addr) -> Self { + Self::Ipv6(addr) + } +} + +impl<'a> From<&'a str> for Host { + fn from(name: &'a str) -> Self { + Self::Name(name.to_string()) + } +} + +impl From for Host { + fn from(name: String) -> Self { + Self::Name(name) + } +} + +impl fmt::Display for Host { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Ipv4(addr) => write!(f, "{addr}"), + Self::Ipv6(addr) => write!(f, "{addr}"), + Self::Name(name) => write!(f, "{name}"), + } + } +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub enum HostParseError { EmptyLabel, EndsWithHyphen, @@ -103,6 +136,8 @@ impl fmt::Display for HostParseError { } } +impl std::error::Error for HostParseError {} + impl FromStr for Host { type Err = HostParseError; diff --git a/distant-core-net/src/common/destination/parser.rs b/distant-core-plugin/src/common/destination/parser.rs similarity index 100% rename from distant-core-net/src/common/destination/parser.rs rename to distant-core-plugin/src/common/destination/parser.rs diff --git a/distant-core-net/src/common/map.rs b/distant-core-plugin/src/common/map.rs similarity index 89% rename from distant-core-net/src/common/map.rs rename to distant-core-plugin/src/common/map.rs index 261dd02..3fccc44 100644 --- a/distant-core-net/src/common/map.rs +++ b/distant-core-plugin/src/common/map.rs @@ -1,10 +1,9 @@ -use std::collections::hash_map::Entry; +use std::collections::hash_map::{Entry, IntoIter, Iter, IterMut}; use std::collections::HashMap; use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use derive_more::{Display, Error, From, IntoIterator}; use serde::de::Deserializer; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; @@ -12,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::common::utils::{deserialize_from_str, serialize_to_str}; /// Contains map information for connections and other use cases -#[derive(Clone, Debug, From, IntoIterator, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Map(HashMap); impl Map { @@ -112,18 +111,64 @@ impl fmt::Display for Map { } } -#[derive(Clone, Debug, Display, Error)] +impl From> for Map { + fn from(map: HashMap) -> Self { + Self(map) + } +} + +impl<'a> IntoIterator for &'a Map { + type Item = (&'a String, &'a String); + type IntoIter = Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a mut Map { + type Item = (&'a String, &'a mut String); + type IntoIter = IterMut<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + +impl IntoIterator for Map { + type Item = (String, String); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Clone, Debug)] pub enum MapParseError { - #[display(fmt = "Missing = after key ('{key}')")] MissingEqualsAfterKey { key: String }, - - #[display(fmt = "Key ('{key}') must start with alphabetic character")] KeyMustStartWithAlphabeticCharacter { key: String }, - - #[display(fmt = "Missing closing \" for value")] MissingClosingQuoteForValue, } +impl fmt::Display for MapParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingEqualsAfterKey { key } => { + write!(f, "Missing = after key ('{key}')") + } + Self::KeyMustStartWithAlphabeticCharacter { key } => { + write!(f, "Key ('{key}') must start with alphabetic character") + } + Self::MissingClosingQuoteForValue => { + write!(f, "Missing closing \" for value") + } + } + } +} + +impl std::error::Error for MapParseError {} + impl FromStr for Map { type Err = MapParseError; diff --git a/distant-core-plugin/src/common/utils.rs b/distant-core-plugin/src/common/utils.rs new file mode 100644 index 0000000..f676236 --- /dev/null +++ b/distant-core-plugin/src/common/utils.rs @@ -0,0 +1,46 @@ +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; + +use serde::de::{Deserializer, Error as SerdeError, Visitor}; +use serde::ser::Serializer; + +/// From https://docs.rs/serde_with/1.14.0/src/serde_with/rust.rs.html#90-118 +pub fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromStr, + T::Err: fmt::Display, +{ + struct Helper(PhantomData); + + impl<'de, S> Visitor<'de> for Helper + where + S: FromStr, + ::Err: fmt::Display, + { + type Value = S; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: SerdeError, + { + value.parse::().map_err(SerdeError::custom) + } + } + + deserializer.deserialize_str(Helper(PhantomData)) +} + +/// From https://docs.rs/serde_with/1.14.0/src/serde_with/rust.rs.html#121-127 +pub fn serialize_to_str(value: &T, serializer: S) -> Result +where + T: fmt::Display, + S: Serializer, +{ + serializer.collect_str(&value) +} diff --git a/distant-core-plugin/src/handlers.rs b/distant-core-plugin/src/handlers.rs new file mode 100644 index 0000000..291ccfb --- /dev/null +++ b/distant-core-plugin/src/handlers.rs @@ -0,0 +1,312 @@ +use std::future::Future; +use std::io; + +use async_trait::async_trait; +use distant_core_auth::Authenticator; + +use crate::common::{Destination, Map}; + +/// Boxed [`LaunchHandler`]. +pub type BoxedLaunchHandler = Box; + +/// Boxed [`ConnectHandler`]. +pub type BoxedConnectHandler = Box; + +/// Interface for a handler to launch a server, returning the destination to the server. +#[async_trait] +pub trait LaunchHandler: Send + Sync { + /// Launches a server using the target `destination`. If the destination is unsupported, this + /// method will return an error. + /// + /// * Takes `options` as additional parameters custom to the destination. + /// * Takes `authenticator` to handle any authentication needs. + async fn launch( + &self, + destination: &Destination, + options: &Map, + authenticator: &mut dyn Authenticator, + ) -> io::Result; +} + +#[async_trait] +impl LaunchHandler for F +where + F: Fn(&Destination, &Map, &mut dyn Authenticator) -> R + Send + Sync + 'static, + R: Future> + Send + 'static, +{ + async fn launch( + &self, + destination: &Destination, + options: &Map, + authenticator: &mut dyn Authenticator, + ) -> io::Result { + self(destination, options, authenticator).await + } +} + +/// Generates a new [`LaunchHandler`] for the provided anonymous function. +/// +/// ### Examples +/// +/// ``` +/// use distant_core_plugin::boxed_launch_handler; +/// +/// let _handler = boxed_launch_handler!(|destination, options, authenticator| { +/// todo!("Implement handler logic."); +/// }); +/// +/// let _handler = boxed_launch_handler!(|destination, options, authenticator| async { +/// todo!("We support async within as well regardless of the keyword!"); +/// }); +/// +/// let _handler = boxed_launch_handler!(move |destination, options, authenticator| { +/// todo!("You can also explicitly mark to move into the closure"); +/// }); +/// ``` +#[macro_export] +macro_rules! boxed_launch_handler { + (|$destination:ident, $options:ident, $authenticator:ident| $(async)? $body:block) => {{ + let x: $crate::handlers::BoxedLaunchHandler = Box::new( + |$destination: &$crate::common::Destination, + $options: &$crate::common::Map, + $authenticator: &mut dyn $crate::auth::Authenticator| async { $body }, + ); + x + }}; + (move |$destination:ident, $options:ident, $authenticator:ident| $(async)? $body:block) => {{ + let x: $crate::handlers::BoxedLaunchHandler = Box::new( + move |$destination: &$crate::common::Destination, + $options: &$crate::common::Map, + $authenticator: &mut dyn $crate::auth::Authenticator| async move { $body }, + ); + x + }}; +} + +/// Interface for a handler to connect to a server, returning a boxed client to the server. +#[async_trait] +pub trait ConnectHandler: Send + Sync { + /// Connects to a server at the specified `destination`. If the destination is unsupported, + /// this method will return an error. + /// + /// * Takes `options` as additional parameters custom to the destination. + /// * Takes `authenticator` to handle any authentication needs. + async fn connect( + &self, + destination: &Destination, + options: &Map, + authenticator: &mut dyn Authenticator, + ) -> io::Result; +} + +#[async_trait] +impl ConnectHandler for F +where + F: Fn(&Destination, &Map, &mut dyn Authenticator) -> R + Send + Sync + 'static, + R: Future> + Send + 'static, +{ + async fn connect( + &self, + destination: &Destination, + options: &Map, + authenticator: &mut dyn Authenticator, + ) -> io::Result { + self(destination, options, authenticator).await + } +} + +/// Generates a new [`ConnectHandler`] for the provided anonymous function. +/// +/// ### Examples +/// +/// ``` +/// use distant_core_plugin::boxed_connect_handler; +/// +/// let _handler = boxed_connect_handler!(|destination, options, authenticator| { +/// todo!("Implement handler logic."); +/// }); +/// +/// let _handler = boxed_connect_handler!(|destination, options, authenticator| async { +/// todo!("We support async within as well regardless of the keyword!"); +/// }); +/// +/// let _handler = boxed_connect_handler!(move |destination, options, authenticator| { +/// todo!("You can also explicitly mark to move into the closure"); +/// }); +/// ``` +#[macro_export] +macro_rules! boxed_connect_handler { + (|$destination:ident, $options:ident, $authenticator:ident| $(async)? $body:block) => {{ + let x: $crate::handlers::BoxedConnectHandler = Box::new( + |$destination: &$crate::common::Destination, + $options: &$crate::common::Map, + $authenticator: &mut dyn $crate::auth::Authenticator| async { $body }, + ); + x + }}; + (move |$destination:ident, $options:ident, $authenticator:ident| $(async)? $body:block) => {{ + let x: $crate::handlers::BoxedConnectHandler = Box::new( + move |$destination: &$crate::common::Destination, + $options: &$crate::common::Map, + $authenticator: &mut dyn $crate::auth::Authenticator| async move { $body }, + ); + x + }}; +} + +#[cfg(test)] +mod tests { + use test_log::test; + + use super::*; + use crate::common::FramedTransport; + + #[inline] + fn test_destination() -> Destination { + "scheme://host:1234".parse().unwrap() + } + + #[inline] + fn test_options() -> Map { + Map::default() + } + + #[inline] + fn test_authenticator() -> impl Authenticator { + FramedTransport::pair(1).0 + } + + #[test(tokio::test)] + async fn boxed_launch_handler_should_generate_valid_boxed_launch_handler() { + let handler = boxed_launch_handler!(|_destination, _options, _authenticator| { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .launch( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_launch_handler!(|_destination, _options, _authenticator| async { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .launch( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_launch_handler!(move |_destination, _options, _authenticator| { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .launch( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_launch_handler!(move |_destination, _options, _authenticator| async { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .launch( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + } + + #[test(tokio::test)] + async fn boxed_connect_handler_should_generate_valid_boxed_connect_handler() { + let handler = boxed_connect_handler!(|_destination, _options, _authenticator| { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .connect( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_connect_handler!(|_destination, _options, _authenticator| async { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .connect( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_connect_handler!(move |_destination, _options, _authenticator| { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .connect( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + let handler = boxed_connect_handler!(move |_destination, _options, _authenticator| async { + Err(io::Error::from(io::ErrorKind::Other)) + }); + assert_eq!( + handler + .connect( + &test_destination(), + &test_options(), + &mut test_authenticator() + ) + .await + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + } +} diff --git a/distant-core-plugin/src/lib.rs b/distant-core-plugin/src/lib.rs index a4fe428..1c2d709 100644 --- a/distant-core-plugin/src/lib.rs +++ b/distant-core-plugin/src/lib.rs @@ -3,3 +3,10 @@ #[doc = include_str!("../README.md")] #[cfg(doctest)] pub struct ReadmeDoctests; + +pub mod api; +pub mod common; +pub mod handlers; + +pub use distant_core_auth as auth; +pub use distant_core_protocol as protocol;