More progress

This commit is contained in:
Chip Senkbeil 2023-10-21 23:12:58 -05:00
parent d6c3b0a155
commit c88f96baba
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131
11 changed files with 504 additions and 12 deletions

6
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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 {
}

View File

@ -0,0 +1,6 @@
mod destination;
mod map;
mod utils;
pub use destination::{Destination, Host, HostParseError};
pub use map::{Map, MapParseError};

View File

@ -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<IpAddr> for Host {
}
}
#[derive(Copy, Clone, Debug, Error, Hash, PartialEq, Eq)]
impl From<Ipv4Addr> for Host {
fn from(addr: Ipv4Addr) -> Self {
Self::Ipv4(addr)
}
}
impl From<Ipv6Addr> 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<String> 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;

View File

@ -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<String, String>);
impl Map {
@ -112,18 +111,64 @@ impl fmt::Display for Map {
}
}
#[derive(Clone, Debug, Display, Error)]
impl From<HashMap<String, String>> for Map {
fn from(map: HashMap<String, String>) -> 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<String, String>;
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;

View File

@ -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<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
struct Helper<S>(PhantomData<S>);
impl<'de, S> Visitor<'de> for Helper<S>
where
S: FromStr,
<S as FromStr>::Err: fmt::Display,
{
type Value = S;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: SerdeError,
{
value.parse::<Self::Value>().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<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
serializer.collect_str(&value)
}

View File

@ -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<dyn LaunchHandler>;
/// Boxed [`ConnectHandler`].
pub type BoxedConnectHandler = Box<dyn ConnectHandler>;
/// 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<Destination>;
}
#[async_trait]
impl<F, R> LaunchHandler for F
where
F: Fn(&Destination, &Map, &mut dyn Authenticator) -> R + Send + Sync + 'static,
R: Future<Output = io::Result<Destination>> + Send + 'static,
{
async fn launch(
&self,
destination: &Destination,
options: &Map,
authenticator: &mut dyn Authenticator,
) -> io::Result<Destination> {
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<UntypedClient>;
}
#[async_trait]
impl<F, R> ConnectHandler for F
where
F: Fn(&Destination, &Map, &mut dyn Authenticator) -> R + Send + Sync + 'static,
R: Future<Output = io::Result<UntypedClient>> + Send + 'static,
{
async fn connect(
&self,
destination: &Destination,
options: &Map,
authenticator: &mut dyn Authenticator,
) -> io::Result<UntypedClient> {
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
);
}
}

View File

@ -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;