mirror of https://github.com/chipsenkbeil/distant
Compare commits
No commits in common. 'master' and 'v0.20.0-alpha.9' have entirely different histories.
master
...
v0.20.0-al
@ -1,24 +0,0 @@
|
||||
name: 'Tag latest'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Tag latest and push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
run: |
|
||||
git config user.name "${GITHUB_ACTOR}"
|
||||
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
|
||||
|
||||
origin_url="$(git config --get remote.origin.url)"
|
||||
origin_url="${origin_url/#https:\/\//https:\/\/$GITHUB_TOKEN@}" # add token to URL
|
||||
|
||||
git tag latest --force
|
||||
git push "$origin_url" --tags --force
|
@ -1,28 +0,0 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
issue-inactive-days: '30'
|
||||
issue-comment: >
|
||||
I'm going to lock this issue because it has been closed for _30 days_ ⏳.
|
||||
This helps our maintainers find and focus on the active issues.
|
||||
If you have found a problem that seems similar to this, please open a new
|
||||
issue and complete the issue template so we can capture all the details
|
||||
necessary to investigate further.
|
||||
process-only: 'issues'
|
@ -1,132 +0,0 @@
|
||||
use semver::{Comparator, Op, Prerelease, Version as SemVer};
|
||||
use std::fmt;
|
||||
|
||||
/// Represents a version and compatibility rules.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Version {
|
||||
inner: SemVer,
|
||||
lower: Comparator,
|
||||
upper: Comparator,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// Creates a new version in the form `major.minor.patch` with a ruleset that is used to check
|
||||
/// other versions such that `>=0.1.2, <0.2.0` or `>=1.2.3, <2` depending on whether or not the
|
||||
/// major version is `0`.
|
||||
///
|
||||
/// ```
|
||||
/// use distant_net::common::Version;
|
||||
///
|
||||
/// // Matching versions are compatible
|
||||
/// let a = Version::new(1, 2, 3);
|
||||
/// let b = Version::new(1, 2, 3);
|
||||
/// assert!(a.is_compatible_with(&b));
|
||||
///
|
||||
/// // Version 1.2.3 is compatible with 1.2.4, but not the other way
|
||||
/// let a = Version::new(1, 2, 3);
|
||||
/// let b = Version::new(1, 2, 4);
|
||||
/// assert!(a.is_compatible_with(&b));
|
||||
/// assert!(!b.is_compatible_with(&a));
|
||||
///
|
||||
/// // Version 1.2.3 is compatible with 1.3.0, but not 2
|
||||
/// let a = Version::new(1, 2, 3);
|
||||
/// assert!(a.is_compatible_with(&Version::new(1, 3, 0)));
|
||||
/// assert!(!a.is_compatible_with(&Version::new(2, 0, 0)));
|
||||
///
|
||||
/// // Version 0.1.2 is compatible with 0.1.3, but not the other way
|
||||
/// let a = Version::new(0, 1, 2);
|
||||
/// let b = Version::new(0, 1, 3);
|
||||
/// assert!(a.is_compatible_with(&b));
|
||||
/// assert!(!b.is_compatible_with(&a));
|
||||
///
|
||||
/// // Version 0.1.2 is not compatible with 0.2
|
||||
/// let a = Version::new(0, 1, 2);
|
||||
/// let b = Version::new(0, 2, 0);
|
||||
/// assert!(!a.is_compatible_with(&b));
|
||||
/// assert!(!b.is_compatible_with(&a));
|
||||
/// ```
|
||||
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
|
||||
Self {
|
||||
inner: SemVer::new(major, minor, patch),
|
||||
lower: Comparator {
|
||||
op: Op::GreaterEq,
|
||||
major,
|
||||
minor: Some(minor),
|
||||
patch: Some(patch),
|
||||
pre: Prerelease::EMPTY,
|
||||
},
|
||||
upper: Comparator {
|
||||
op: Op::Less,
|
||||
major: if major == 0 { 0 } else { major + 1 },
|
||||
minor: if major == 0 { Some(minor + 1) } else { None },
|
||||
patch: None,
|
||||
pre: Prerelease::EMPTY,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this version is compatible with another version.
|
||||
pub fn is_compatible_with(&self, other: &Self) -> bool {
|
||||
self.lower.matches(&other.inner) && self.upper.matches(&other.inner)
|
||||
}
|
||||
|
||||
/// Converts from a collection of bytes into a version using the byte form major/minor/patch
|
||||
/// using big endian.
|
||||
pub const fn from_be_bytes(bytes: [u8; 24]) -> Self {
|
||||
Self::new(
|
||||
u64::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
]),
|
||||
u64::from_be_bytes([
|
||||
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
|
||||
bytes[15],
|
||||
]),
|
||||
u64::from_be_bytes([
|
||||
bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22],
|
||||
bytes[23],
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts the version into a byte form of major/minor/patch using big endian.
|
||||
pub const fn to_be_bytes(&self) -> [u8; 24] {
|
||||
let major = self.inner.major.to_be_bytes();
|
||||
let minor = self.inner.minor.to_be_bytes();
|
||||
let patch = self.inner.patch.to_be_bytes();
|
||||
|
||||
[
|
||||
major[0], major[1], major[2], major[3], major[4], major[5], major[6], major[7],
|
||||
minor[0], minor[1], minor[2], minor[3], minor[4], minor[5], minor[6], minor[7],
|
||||
patch[0], patch[1], patch[2], patch[3], patch[4], patch[5], patch[6], patch[7],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Version {
|
||||
/// Default version is `0.0.0`.
|
||||
fn default() -> Self {
|
||||
Self::new(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<semver::Version> for Version {
|
||||
/// Creates a new [`Version`] using the major, minor, and patch information from
|
||||
/// [`semver::Version`].
|
||||
fn from(version: semver::Version) -> Self {
|
||||
let mut this = Self::new(version.major, version.minor, version.patch);
|
||||
this.inner = version;
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for semver::Version {
|
||||
fn from(version: Version) -> Self {
|
||||
version.inner
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{BitAnd, BitOr, BitXor};
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{From, Into, IntoIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumMessage, IntoEnumIterator};
|
||||
|
||||
use super::ManagerCapabilityKind;
|
||||
|
||||
/// Set of supported capabilities for a manager
|
||||
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ManagerCapabilities(#[into_iterator(owned, ref)] HashSet<ManagerCapability>);
|
||||
|
||||
impl ManagerCapabilities {
|
||||
/// Return set of capabilities encompassing all possible capabilities
|
||||
pub fn all() -> Self {
|
||||
Self(
|
||||
ManagerCapabilityKind::iter()
|
||||
.map(ManagerCapability::from)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return empty set of capabilities
|
||||
pub fn none() -> Self {
|
||||
Self(HashSet::new())
|
||||
}
|
||||
|
||||
/// Returns true if the capability with described kind is included
|
||||
pub fn contains(&self, kind: impl AsRef<str>) -> bool {
|
||||
let cap = ManagerCapability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.contains(&cap)
|
||||
}
|
||||
|
||||
/// Adds the specified capability to the set of capabilities
|
||||
///
|
||||
/// * If the set did not have this capability, returns `true`
|
||||
/// * If the set did have this capability, returns `false`
|
||||
pub fn insert(&mut self, cap: impl Into<ManagerCapability>) -> bool {
|
||||
self.0.insert(cap.into())
|
||||
}
|
||||
|
||||
/// Removes the capability with the described kind, returning the capability
|
||||
pub fn take(&mut self, kind: impl AsRef<str>) -> Option<ManagerCapability> {
|
||||
let cap = ManagerCapability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.take(&cap)
|
||||
}
|
||||
|
||||
/// Removes the capability with the described kind, returning true if it existed
|
||||
pub fn remove(&mut self, kind: impl AsRef<str>) -> bool {
|
||||
let cap = ManagerCapability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.remove(&cap)
|
||||
}
|
||||
|
||||
/// Converts into vec of capabilities sorted by kind
|
||||
pub fn into_sorted_vec(self) -> Vec<ManagerCapability> {
|
||||
let mut this = self.0.into_iter().collect::<Vec<_>>();
|
||||
|
||||
this.sort_unstable();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAnd for &ManagerCapabilities {
|
||||
type Output = ManagerCapabilities;
|
||||
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
ManagerCapabilities(self.0.bitand(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for &ManagerCapabilities {
|
||||
type Output = ManagerCapabilities;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
ManagerCapabilities(self.0.bitor(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr<ManagerCapability> for &ManagerCapabilities {
|
||||
type Output = ManagerCapabilities;
|
||||
|
||||
fn bitor(self, rhs: ManagerCapability) -> Self::Output {
|
||||
let mut other = ManagerCapabilities::none();
|
||||
other.0.insert(rhs);
|
||||
|
||||
self.bitor(&other)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitXor for &ManagerCapabilities {
|
||||
type Output = ManagerCapabilities;
|
||||
|
||||
fn bitxor(self, rhs: Self) -> Self::Output {
|
||||
ManagerCapabilities(self.0.bitxor(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<ManagerCapability> for ManagerCapabilities {
|
||||
fn from_iter<I: IntoIterator<Item = ManagerCapability>>(iter: I) -> Self {
|
||||
let mut this = ManagerCapabilities::none();
|
||||
|
||||
for capability in iter {
|
||||
this.0.insert(capability);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// ManagerCapability tied to a manager. A capability is equivalent based on its kind and not
|
||||
/// description.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub struct ManagerCapability {
|
||||
/// Label describing the kind of capability
|
||||
pub kind: String,
|
||||
|
||||
/// Information about the capability
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl ManagerCapability {
|
||||
/// Will convert the [`ManagerCapability`]'s `kind` into a known [`ManagerCapabilityKind`] if
|
||||
/// possible, returning None if the capability is unknown
|
||||
pub fn to_capability_kind(&self) -> Option<ManagerCapabilityKind> {
|
||||
ManagerCapabilityKind::from_str(&self.kind).ok()
|
||||
}
|
||||
|
||||
/// Returns true if the described capability is unknown
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
self.to_capability_kind().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ManagerCapability {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.kind.eq_ignore_ascii_case(&other.kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ManagerCapability {}
|
||||
|
||||
impl PartialOrd for ManagerCapability {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ManagerCapability {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.kind
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&other.kind.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ManagerCapability {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.kind.to_ascii_lowercase().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManagerCapabilityKind> for ManagerCapability {
|
||||
/// Creates a new capability using the kind's default message
|
||||
fn from(kind: ManagerCapabilityKind) -> Self {
|
||||
Self {
|
||||
kind: kind.to_string(),
|
||||
description: kind
|
||||
.get_message()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,380 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{BitAnd, BitOr, BitXor, Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{From, Into, IntoIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumMessage, IntoEnumIterator};
|
||||
|
||||
/// Represents the kinds of capabilities available.
|
||||
pub use crate::request::RequestKind as CapabilityKind;
|
||||
|
||||
/// Set of supported capabilities for a server
|
||||
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Capabilities(#[into_iterator(owned, ref)] HashSet<Capability>);
|
||||
|
||||
impl Capabilities {
|
||||
/// Return set of capabilities encompassing all possible capabilities
|
||||
pub fn all() -> Self {
|
||||
Self(CapabilityKind::iter().map(Capability::from).collect())
|
||||
}
|
||||
|
||||
/// Return empty set of capabilities
|
||||
pub fn none() -> Self {
|
||||
Self(HashSet::new())
|
||||
}
|
||||
|
||||
/// Returns true if the capability with described kind is included
|
||||
pub fn contains(&self, kind: impl AsRef<str>) -> bool {
|
||||
let cap = Capability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.contains(&cap)
|
||||
}
|
||||
|
||||
/// Adds the specified capability to the set of capabilities
|
||||
///
|
||||
/// * If the set did not have this capability, returns `true`
|
||||
/// * If the set did have this capability, returns `false`
|
||||
pub fn insert(&mut self, cap: impl Into<Capability>) -> bool {
|
||||
self.0.insert(cap.into())
|
||||
}
|
||||
|
||||
/// Removes the capability with the described kind, returning the capability
|
||||
pub fn take(&mut self, kind: impl AsRef<str>) -> Option<Capability> {
|
||||
let cap = Capability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.take(&cap)
|
||||
}
|
||||
|
||||
/// Removes the capability with the described kind, returning true if it existed
|
||||
pub fn remove(&mut self, kind: impl AsRef<str>) -> bool {
|
||||
let cap = Capability {
|
||||
kind: kind.as_ref().to_string(),
|
||||
description: String::new(),
|
||||
};
|
||||
self.0.remove(&cap)
|
||||
}
|
||||
|
||||
/// Converts into vec of capabilities sorted by kind
|
||||
pub fn into_sorted_vec(self) -> Vec<Capability> {
|
||||
let mut this = self.0.into_iter().collect::<Vec<_>>();
|
||||
|
||||
this.sort_unstable();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<HashSet<Capability>> for Capabilities {
|
||||
fn as_ref(&self) -> &HashSet<Capability> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<HashSet<Capability>> for Capabilities {
|
||||
fn as_mut(&mut self) -> &mut HashSet<Capability> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Capabilities {
|
||||
type Target = HashSet<Capability>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Capabilities {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAnd for &Capabilities {
|
||||
type Output = Capabilities;
|
||||
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
Capabilities(self.0.bitand(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for &Capabilities {
|
||||
type Output = Capabilities;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Capabilities(self.0.bitor(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr<Capability> for &Capabilities {
|
||||
type Output = Capabilities;
|
||||
|
||||
fn bitor(self, rhs: Capability) -> Self::Output {
|
||||
let mut other = Capabilities::none();
|
||||
other.0.insert(rhs);
|
||||
|
||||
self.bitor(&other)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitXor for &Capabilities {
|
||||
type Output = Capabilities;
|
||||
|
||||
fn bitxor(self, rhs: Self) -> Self::Output {
|
||||
Capabilities(self.0.bitxor(&rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Capability> for Capabilities {
|
||||
fn from_iter<I: IntoIterator<Item = Capability>>(iter: I) -> Self {
|
||||
let mut this = Capabilities::none();
|
||||
|
||||
for capability in iter {
|
||||
this.0.insert(capability);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability tied to a server. A capability is equivalent based on its kind and not description.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub struct Capability {
|
||||
/// Label describing the kind of capability
|
||||
pub kind: String,
|
||||
|
||||
/// Information about the capability
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
/// Will convert the [`Capability`]'s `kind` into a known [`CapabilityKind`] if possible,
|
||||
/// returning None if the capability is unknown
|
||||
pub fn to_capability_kind(&self) -> Option<CapabilityKind> {
|
||||
CapabilityKind::from_str(&self.kind).ok()
|
||||
}
|
||||
|
||||
/// Returns true if the described capability is unknown
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
self.to_capability_kind().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Capability {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.kind.eq_ignore_ascii_case(&other.kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Capability {}
|
||||
|
||||
impl PartialOrd for Capability {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Capability {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.kind
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&other.kind.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Capability {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.kind.to_ascii_lowercase().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CapabilityKind> for Capability {
|
||||
/// Creates a new capability using the kind's default message
|
||||
fn from(kind: CapabilityKind) -> Self {
|
||||
Self {
|
||||
kind: kind.to_string(),
|
||||
description: kind
|
||||
.get_message()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod capabilities {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_serialize_to_json() {
|
||||
let capabilities: Capabilities = [Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let value = serde_json::to_value(capabilities).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!([
|
||||
{
|
||||
"kind": "some kind",
|
||||
"description": "some description",
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_deserialize_from_json() {
|
||||
let value = serde_json::json!([
|
||||
{
|
||||
"kind": "some kind",
|
||||
"description": "some description",
|
||||
}
|
||||
]);
|
||||
|
||||
let capabilities: Capabilities = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(
|
||||
capabilities,
|
||||
[Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_serialize_to_msgpack() {
|
||||
let capabilities: Capabilities = [Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// NOTE: We don't actually check the output here because it's an implementation detail
|
||||
// and could change as we change how serialization is done. This is merely to verify
|
||||
// that we can serialize since there are times when serde fails to serialize at
|
||||
// runtime.
|
||||
let _ = rmp_serde::encode::to_vec_named(&capabilities).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_deserialize_from_msgpack() {
|
||||
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
|
||||
// verify that we are not corrupting or preventing issues when serializing on a
|
||||
// client/server and then trying to deserialize on the other side. This has happened
|
||||
// enough times with minor changes that we need tests to verify.
|
||||
let buf = rmp_serde::encode::to_vec_named(
|
||||
&[Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect::<Capabilities>(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let capabilities: Capabilities = rmp_serde::decode::from_slice(&buf).unwrap();
|
||||
assert_eq!(
|
||||
capabilities,
|
||||
[Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod capability {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_serialize_to_json() {
|
||||
let capability = Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(capability).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!({
|
||||
"kind": "some kind",
|
||||
"description": "some description",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_deserialize_from_json() {
|
||||
let value = serde_json::json!({
|
||||
"kind": "some kind",
|
||||
"description": "some description",
|
||||
});
|
||||
|
||||
let capability: Capability = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(
|
||||
capability,
|
||||
Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_serialize_to_msgpack() {
|
||||
let capability = Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
};
|
||||
|
||||
// NOTE: We don't actually check the output here because it's an implementation detail
|
||||
// and could change as we change how serialization is done. This is merely to verify
|
||||
// that we can serialize since there are times when serde fails to serialize at
|
||||
// runtime.
|
||||
let _ = rmp_serde::encode::to_vec_named(&capability).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_able_to_deserialize_from_msgpack() {
|
||||
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
|
||||
// verify that we are not corrupting or causing issues when serializing on a
|
||||
// client/server and then trying to deserialize on the other side. This has happened
|
||||
// enough times with minor changes that we need tests to verify.
|
||||
let buf = rmp_serde::encode::to_vec_named(&Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let capability: Capability = rmp_serde::decode::from_slice(&buf).unwrap();
|
||||
assert_eq!(
|
||||
capability,
|
||||
Capability {
|
||||
kind: "some kind".to_string(),
|
||||
description: "some description".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
use derive_more::{Display, Error};
|
||||
use std::str::FromStr;
|
||||
use typed_path::{Utf8UnixPath, Utf8WindowsPath};
|
||||
|
||||
/// Represents a shell to execute on the remote machine.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Shell {
|
||||
/// Represents the path to the shell on the remote machine.
|
||||
pub path: String,
|
||||
|
||||
/// Represents the kind of shell.
|
||||
pub kind: ShellKind,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
#[inline]
|
||||
pub fn is_posix(&self) -> bool {
|
||||
self.kind.is_posix()
|
||||
}
|
||||
|
||||
/// Wraps a `cmd` such that it is invoked by this shell.
|
||||
///
|
||||
/// * For `cmd.exe`, this wraps in double quotes such that it can be invoked by `cmd.exe /S /C "..."`.
|
||||
/// * For `powershell.exe`, this wraps in single quotes and escapes single quotes by doubling
|
||||
/// them such that it can be invoked by `powershell.exe -Command '...'`.
|
||||
/// * For `rc` and `elvish`, this wraps in single quotes and escapes single quotes by doubling them.
|
||||
/// * For rc and elvish, this uses `shell -c '...'`.
|
||||
/// * For **POSIX** shells, this wraps in single quotes and uses the trick of `'\''` to fake escape.
|
||||
/// * For `nu`, this wraps in single quotes or backticks where possible, but fails if the cmd contains single quotes and backticks.
|
||||
///
|
||||
pub fn make_cmd_string(&self, cmd: &str) -> Result<String, &'static str> {
|
||||
let path = self.path.as_str();
|
||||
|
||||
match self.kind {
|
||||
ShellKind::CmdExe => Ok(format!("{path} /S /C \"{cmd}\"")),
|
||||
|
||||
// NOTE: Powershell does not work directly because our splitting logic for arguments on
|
||||
// distant-local does not handle single quotes. In fact, the splitting logic
|
||||
// isn't designed for powershell at all. To get around that limitation, we are
|
||||
// using cmd.exe to invoke powershell, which fits closer to our parsing rules.
|
||||
// Crazy, I know! Eventually, we should switch to properly using powershell
|
||||
// and escaping single quotes by doubling them.
|
||||
ShellKind::PowerShell => Ok(format!(
|
||||
"cmd.exe /S /C \"{path} -Command {}\"",
|
||||
cmd.replace('"', "\"\""),
|
||||
)),
|
||||
|
||||
ShellKind::Rc | ShellKind::Elvish => {
|
||||
Ok(format!("{path} -c '{}'", cmd.replace('\'', "''")))
|
||||
}
|
||||
|
||||
ShellKind::Nu => {
|
||||
let has_single_quotes = cmd.contains('\'');
|
||||
let has_backticks = cmd.contains('`');
|
||||
|
||||
match (has_single_quotes, has_backticks) {
|
||||
// If we have both single quotes and backticks, fail
|
||||
(true, true) => {
|
||||
Err("unable to escape single quotes and backticks at the same time with nu")
|
||||
}
|
||||
|
||||
// If we only have single quotes, use backticks
|
||||
(true, false) => Ok(format!("{path} -c `{cmd}`")),
|
||||
|
||||
// Otherwise, we can safely use single quotes
|
||||
_ => Ok(format!("{path} -c '{cmd}'")),
|
||||
}
|
||||
}
|
||||
|
||||
// We assume anything else not specially handled is POSIX
|
||||
_ => Ok(format!("{path} -c '{}'", cmd.replace('\'', "'\\''"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Display, Error)]
|
||||
pub struct ParseShellError(#[error(not(source))] String);
|
||||
|
||||
impl FromStr for Shell {
|
||||
type Err = ParseShellError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
|
||||
let kind = ShellKind::identify(s)
|
||||
.ok_or_else(|| ParseShellError(format!("Unsupported shell: {s}")))?;
|
||||
|
||||
Ok(Self {
|
||||
path: s.to_string(),
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported types of shells.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellKind {
|
||||
Ash,
|
||||
Bash,
|
||||
CmdExe,
|
||||
Csh,
|
||||
Dash,
|
||||
Elvish,
|
||||
Fish,
|
||||
Ksh,
|
||||
Loksh,
|
||||
Mksh,
|
||||
Nu,
|
||||
Pdksh,
|
||||
PowerShell,
|
||||
Rc,
|
||||
Scsh,
|
||||
Sh,
|
||||
Tcsh,
|
||||
Zsh,
|
||||
}
|
||||
|
||||
impl ShellKind {
|
||||
/// Returns true if shell represents a POSIX-compliant implementation.
|
||||
pub fn is_posix(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Ash
|
||||
| Self::Bash
|
||||
| Self::Csh
|
||||
| Self::Dash
|
||||
| Self::Fish
|
||||
| Self::Ksh
|
||||
| Self::Loksh
|
||||
| Self::Mksh
|
||||
| Self::Pdksh
|
||||
| Self::Scsh
|
||||
| Self::Sh
|
||||
| Self::Tcsh
|
||||
| Self::Zsh
|
||||
)
|
||||
}
|
||||
|
||||
/// Identifies the shell kind from the given string. This string could be a Windows path, Unix
|
||||
/// path, or solo shell name.
|
||||
///
|
||||
/// The process is handled by these steps:
|
||||
///
|
||||
/// 1. Check if the string matches a shell name verbatim
|
||||
/// 2. Parse the path as a Unix path and check the file name for a match
|
||||
/// 3. Parse the path as a Windows path and check the file name for a match
|
||||
///
|
||||
pub fn identify(s: &str) -> Option<Self> {
|
||||
Self::from_name(s)
|
||||
.or_else(|| Utf8UnixPath::new(s).file_name().and_then(Self::from_name))
|
||||
.or_else(|| {
|
||||
Utf8WindowsPath::new(s)
|
||||
.file_name()
|
||||
.and_then(Self::from_name)
|
||||
})
|
||||
}
|
||||
|
||||
fn from_name(name: &str) -> Option<Self> {
|
||||
macro_rules! map_str {
|
||||
($($name:literal -> $value:expr),+ $(,)?) => {{
|
||||
$(
|
||||
if name.trim().eq_ignore_ascii_case($name) {
|
||||
return Some($value);
|
||||
}
|
||||
|
||||
)+
|
||||
|
||||
None
|
||||
}};
|
||||
}
|
||||
|
||||
map_str! {
|
||||
"ash" -> Self::Ash,
|
||||
"bash" -> Self::Bash,
|
||||
"cmd" -> Self::CmdExe,
|
||||
"cmd.exe" -> Self::CmdExe,
|
||||
"csh" -> Self::Csh,
|
||||
"dash" -> Self::Dash,
|
||||
"elvish" -> Self::Elvish,
|
||||
"fish" -> Self::Fish,
|
||||
"ksh" -> Self::Ksh,
|
||||
"loksh" -> Self::Loksh,
|
||||
"mksh" -> Self::Mksh,
|
||||
"nu" -> Self::Nu,
|
||||
"pdksh" -> Self::Pdksh,
|
||||
"powershell" -> Self::PowerShell,
|
||||
"powershell.exe" -> Self::PowerShell,
|
||||
"rc" -> Self::Rc,
|
||||
"scsh" -> Self::Scsh,
|
||||
"sh" -> Self::Sh,
|
||||
"tcsh" -> Self::Tcsh,
|
||||
"zsh" -> Self::Zsh,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
use indoc::indoc;
|
||||
use rstest::*;
|
||||
|
||||
use crate::common::fixtures::*;
|
||||
|
||||
const EXPECTED_TABLE: &str = indoc! {"
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| kind | description |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| authenticate | Supports authenticating with a remote server |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| capabilities | Supports retrieving capabilities |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| channel | Supports sending data through a channel with a remote server |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| close_channel | Supports closing a channel with a remote server |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| connect | Supports connecting to remote servers |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| info | Supports retrieving connection-specific information |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| kill | Supports killing a remote connection |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| launch | Supports launching a server on remote machines |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| list | Supports retrieving a list of managed connections |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
| open_channel | Supports opening a channel with a remote server |
|
||||
+---------------+--------------------------------------------------------------+
|
||||
"};
|
||||
|
||||
#[rstest]
|
||||
#[test_log::test]
|
||||
fn should_output_capabilities(ctx: DistantManagerCtx) {
|
||||
// distant action capabilities
|
||||
ctx.new_assert_cmd(vec!["manager", "capabilities"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(EXPECTED_TABLE)
|
||||
.stderr("");
|
||||
}
|
@ -1 +1 @@
|
||||
mod version;
|
||||
mod capabilities;
|
||||
|
@ -1,13 +0,0 @@
|
||||
use rstest::*;
|
||||
|
||||
use crate::common::fixtures::*;
|
||||
|
||||
#[rstest]
|
||||
#[test_log::test]
|
||||
fn should_output_version(ctx: DistantManagerCtx) {
|
||||
ctx.new_assert_cmd(vec!["manager", "version"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(format!("{}\n", env!("CARGO_PKG_VERSION")))
|
||||
.stderr("");
|
||||
}
|
Loading…
Reference in New Issue