mirror of https://github.com/chipsenkbeil/distant
Compare commits
No commits in common. 'master' and 'v0.20.0-alpha.8' 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,325 +0,0 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use distant_core::{
|
||||
DistantApi, DistantApiServerHandler, DistantChannelExt, DistantClient, DistantCtx,
|
||||
};
|
||||
use distant_net::auth::{DummyAuthHandler, Verifier};
|
||||
use distant_net::client::Client;
|
||||
use distant_net::common::{InmemoryTransport, OneshotListener, Version};
|
||||
use distant_net::server::{Server, ServerRef};
|
||||
use distant_protocol::PROTOCOL_VERSION;
|
||||
|
||||
/// Stands up an inmemory client and server using the given api.
|
||||
async fn setup(api: impl DistantApi + Send + Sync + 'static) -> (DistantClient, ServerRef) {
|
||||
let (t1, t2) = InmemoryTransport::pair(100);
|
||||
|
||||
let server = Server::new()
|
||||
.handler(DistantApiServerHandler::new(api))
|
||||
.verifier(Verifier::none())
|
||||
.version(Version::new(
|
||||
PROTOCOL_VERSION.major,
|
||||
PROTOCOL_VERSION.minor,
|
||||
PROTOCOL_VERSION.patch,
|
||||
))
|
||||
.start(OneshotListener::from_value(t2))
|
||||
.expect("Failed to start server");
|
||||
|
||||
let client: DistantClient = Client::build()
|
||||
.auth_handler(DummyAuthHandler)
|
||||
.connector(t1)
|
||||
.version(Version::new(
|
||||
PROTOCOL_VERSION.major,
|
||||
PROTOCOL_VERSION.minor,
|
||||
PROTOCOL_VERSION.patch,
|
||||
))
|
||||
.connect()
|
||||
.await
|
||||
.expect("Failed to connect to server");
|
||||
|
||||
(client, server)
|
||||
}
|
||||
|
||||
mod single {
|
||||
use test_log::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_support_single_request_returning_error() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, _path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "test error"))
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let error = client.read_file(PathBuf::from("file")).await.unwrap_err();
|
||||
assert_eq!(error.kind(), io::ErrorKind::NotFound);
|
||||
assert_eq!(error.to_string(), "test error");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_support_single_request_returning_success() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, _path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
Ok(b"hello world".to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let contents = client.read_file(PathBuf::from("file")).await.unwrap();
|
||||
assert_eq!(contents, b"hello world");
|
||||
}
|
||||
}
|
||||
|
||||
mod batch_parallel {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use distant_net::common::Request;
|
||||
use distant_protocol::{Msg, Request as RequestPayload};
|
||||
use test_log::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_support_multiple_requests_running_in_parallel() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
if path.to_str().unwrap() == "slow" {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
Ok((time.as_millis() as u64).to_be_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let request = Request::new(Msg::batch([
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file1"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("slow"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file2"),
|
||||
},
|
||||
]));
|
||||
|
||||
let response = client.send(request).await.unwrap();
|
||||
let payloads = response.payload.into_batch().unwrap();
|
||||
|
||||
// Collect our times from the reading
|
||||
let mut times = Vec::new();
|
||||
for payload in payloads {
|
||||
match payload {
|
||||
distant_protocol::Response::Blob { data } => {
|
||||
let mut buf = [0u8; 8];
|
||||
buf.copy_from_slice(&data[..8]);
|
||||
times.push(u64::from_be_bytes(buf));
|
||||
}
|
||||
x => panic!("Unexpected payload: {x:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that these ran in parallel as the first and third requests should not be
|
||||
// over 500 milliseconds apart due to the sleep in the middle!
|
||||
let diff = times[0].abs_diff(times[2]);
|
||||
assert!(diff <= 500, "Sequential ordering detected");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_run_all_requests_even_if_some_fail() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
if path.to_str().unwrap() == "fail" {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "test error"));
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let request = Request::new(Msg::batch([
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file1"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("fail"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file2"),
|
||||
},
|
||||
]));
|
||||
|
||||
let response = client.send(request).await.unwrap();
|
||||
let payloads = response.payload.into_batch().unwrap();
|
||||
|
||||
// Should be a success, error, and success
|
||||
assert!(
|
||||
matches!(payloads[0], distant_protocol::Response::Blob { .. }),
|
||||
"Unexpected payloads[0]: {:?}",
|
||||
payloads[0]
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
&payloads[1],
|
||||
distant_protocol::Response::Error(distant_protocol::Error { kind, description })
|
||||
if matches!(kind, distant_protocol::ErrorKind::Other) && description == "test error"
|
||||
),
|
||||
"Unexpected payloads[1]: {:?}",
|
||||
payloads[1]
|
||||
);
|
||||
assert!(
|
||||
matches!(payloads[2], distant_protocol::Response::Blob { .. }),
|
||||
"Unexpected payloads[2]: {:?}",
|
||||
payloads[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod batch_sequence {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use distant_net::common::Request;
|
||||
use distant_protocol::{Msg, Request as RequestPayload};
|
||||
use test_log::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_support_multiple_requests_running_in_sequence() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
if path.to_str().unwrap() == "slow" {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
Ok((time.as_millis() as u64).to_be_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let mut request = Request::new(Msg::batch([
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file1"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("slow"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file2"),
|
||||
},
|
||||
]));
|
||||
|
||||
// Mark as running in sequence
|
||||
request.header.insert("sequence", true);
|
||||
|
||||
let response = client.send(request).await.unwrap();
|
||||
let payloads = response.payload.into_batch().unwrap();
|
||||
|
||||
// Collect our times from the reading
|
||||
let mut times = Vec::new();
|
||||
for payload in payloads {
|
||||
match payload {
|
||||
distant_protocol::Response::Blob { data } => {
|
||||
let mut buf = [0u8; 8];
|
||||
buf.copy_from_slice(&data[..8]);
|
||||
times.push(u64::from_be_bytes(buf));
|
||||
}
|
||||
x => panic!("Unexpected payload: {x:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that these ran in sequence as the first and third requests should be
|
||||
// over 500 milliseconds apart due to the sleep in the middle!
|
||||
let diff = times[0].abs_diff(times[2]);
|
||||
assert!(diff > 500, "Parallel ordering detected");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn should_interrupt_any_requests_following_a_failure() {
|
||||
struct TestDistantApi;
|
||||
|
||||
#[async_trait]
|
||||
impl DistantApi for TestDistantApi {
|
||||
async fn read_file(&self, _ctx: DistantCtx, path: PathBuf) -> io::Result<Vec<u8>> {
|
||||
if path.to_str().unwrap() == "fail" {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "test error"));
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
let (mut client, _server) = setup(TestDistantApi).await;
|
||||
|
||||
let mut request = Request::new(Msg::batch([
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file1"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("fail"),
|
||||
},
|
||||
RequestPayload::FileRead {
|
||||
path: PathBuf::from("file2"),
|
||||
},
|
||||
]));
|
||||
|
||||
// Mark as running in sequence
|
||||
request.header.insert("sequence", true);
|
||||
|
||||
let response = client.send(request).await.unwrap();
|
||||
let payloads = response.payload.into_batch().unwrap();
|
||||
|
||||
// Should be a success, error, and interrupt
|
||||
assert!(
|
||||
matches!(payloads[0], distant_protocol::Response::Blob { .. }),
|
||||
"Unexpected payloads[0]: {:?}",
|
||||
payloads[0]
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
&payloads[1],
|
||||
distant_protocol::Response::Error(distant_protocol::Error { kind, description })
|
||||
if matches!(kind, distant_protocol::ErrorKind::Other) && description == "test error"
|
||||
),
|
||||
"Unexpected payloads[1]: {:?}",
|
||||
payloads[1]
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
&payloads[2],
|
||||
distant_protocol::Response::Error(distant_protocol::Error { kind, .. })
|
||||
if matches!(kind, distant_protocol::ErrorKind::Interrupted)
|
||||
),
|
||||
"Unexpected payloads[2]: {:?}",
|
||||
payloads[2]
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,109 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{fmt, io};
|
||||
|
||||
use derive_more::IntoIterator;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::{utils, Value};
|
||||
|
||||
/// Generates a new [`Header`] of key/value pairs based on literals.
|
||||
///
|
||||
/// ```
|
||||
/// use distant_net::header;
|
||||
///
|
||||
/// let _header = header!("key" -> "value", "key2" -> 123);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! header {
|
||||
($($key:literal -> $value:expr),* $(,)?) => {{
|
||||
let mut _header = $crate::common::Header::default();
|
||||
|
||||
$(
|
||||
_header.insert($key, $value);
|
||||
)*
|
||||
|
||||
_header
|
||||
}};
|
||||
}
|
||||
|
||||
/// Represents a packet header comprised of arbitrary data tied to string keys.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Header(HashMap<String, Value>);
|
||||
|
||||
impl Header {
|
||||
/// Creates an empty [`Header`] newtype wrapper.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Exists purely to support serde serialization checks.
|
||||
#[inline]
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Inserts a key-value pair into the map.
|
||||
///
|
||||
/// If the map did not have this key present, [`None`] is returned.
|
||||
///
|
||||
/// If the map did have this key present, the value is updated, and the old value is returned.
|
||||
/// The key is not updated, though; this matters for types that can be `==` without being
|
||||
/// identical. See the [module-level documentation](std::collections#insert-and-complex-keys)
|
||||
/// for more.
|
||||
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
|
||||
self.0.insert(key.into(), value.into())
|
||||
}
|
||||
|
||||
/// Retrieves a value from the header, attempting to convert it to the specified type `T`
|
||||
/// by cloning the value and then converting it.
|
||||
pub fn get_as<T>(&self, key: impl AsRef<str>) -> Option<io::Result<T>>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
self.0
|
||||
.get(key.as_ref())
|
||||
.map(|value| value.clone().cast_as())
|
||||
}
|
||||
|
||||
/// Serializes the header into bytes.
|
||||
pub fn to_vec(&self) -> io::Result<Vec<u8>> {
|
||||
utils::serialize_to_vec(self)
|
||||
}
|
||||
|
||||
/// Deserializes the header from bytes.
|
||||
pub fn from_slice(slice: &[u8]) -> io::Result<Self> {
|
||||
utils::deserialize_from_slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Header {
|
||||
type Target = HashMap<String, Value>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Header {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Header {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{{")?;
|
||||
|
||||
for (key, value) in self.0.iter() {
|
||||
let value = serde_json::to_string(value).unwrap_or_else(|_| String::from("--"));
|
||||
write!(f, "\"{key}\" = {value}")?;
|
||||
}
|
||||
|
||||
write!(f, "}}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
use std::io;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::utils;
|
||||
|
||||
/// Generic value type for data passed through header.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Value(serde_json::Value);
|
||||
|
||||
impl Value {
|
||||
/// Creates a new [`Value`] by converting `value` to the underlying type.
|
||||
pub fn new(value: impl Into<serde_json::Value>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
/// Serializes the value into bytes.
|
||||
pub fn to_vec(&self) -> io::Result<Vec<u8>> {
|
||||
utils::serialize_to_vec(self)
|
||||
}
|
||||
|
||||
/// Deserializes the value from bytes.
|
||||
pub fn from_slice(slice: &[u8]) -> io::Result<Self> {
|
||||
utils::deserialize_from_slice(slice)
|
||||
}
|
||||
|
||||
/// Attempts to convert this generic value to a specific type.
|
||||
pub fn cast_as<T>(self) -> io::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_value(self.0).map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Value {
|
||||
type Target = serde_json::Value;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Value {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($($type:ty),+) => {
|
||||
$(
|
||||
impl From<$type> for Value {
|
||||
fn from(x: $type) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(
|
||||
(),
|
||||
i8, i16, i32, i64, isize,
|
||||
u8, u16, u32, u64, usize,
|
||||
f32, f64,
|
||||
bool, String, serde_json::Number,
|
||||
serde_json::Map<String, serde_json::Value>
|
||||
);
|
||||
|
||||
impl<'a, T> From<&'a [T]> for Value
|
||||
where
|
||||
T: Clone + Into<serde_json::Value>,
|
||||
{
|
||||
fn from(x: &'a [T]) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Value {
|
||||
fn from(x: &'a str) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Value {
|
||||
fn from(x: Cow<'a, str>) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for Value
|
||||
where
|
||||
T: Into<serde_json::Value>,
|
||||
{
|
||||
fn from(x: Option<T>) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for Value
|
||||
where
|
||||
T: Into<serde_json::Value>,
|
||||
{
|
||||
fn from(x: Vec<T>) -> Self {
|
||||
Self(From::from(x))
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +1,28 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ServerReply;
|
||||
use crate::common::{ConnectionId, Request};
|
||||
|
||||
/// Represents contextual information for working with an inbound request.
|
||||
pub struct RequestCtx<T, U> {
|
||||
/// Unique identifer associated with the connection that sent the request.
|
||||
/// Represents contextual information for working with an inbound request
|
||||
pub struct ServerCtx<T, U, D> {
|
||||
/// Unique identifer associated with the connection that sent the request
|
||||
pub connection_id: ConnectionId,
|
||||
|
||||
/// The request being handled.
|
||||
/// The request being handled
|
||||
pub request: Request<T>,
|
||||
|
||||
/// Used to send replies back to be sent out by the server.
|
||||
/// Used to send replies back to be sent out by the server
|
||||
pub reply: ServerReply<U>,
|
||||
|
||||
/// Reference to the connection's local data
|
||||
pub local_data: Arc<D>,
|
||||
}
|
||||
|
||||
impl<T, U> fmt::Debug for RequestCtx<T, U>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RequestCtx")
|
||||
.field("connection_id", &self.connection_id)
|
||||
.field("request", &self.request)
|
||||
.field("reply", &"...")
|
||||
.finish()
|
||||
}
|
||||
/// Represents contextual information for working with an inbound connection
|
||||
pub struct ConnectionCtx<'a, D> {
|
||||
/// Unique identifer associated with the connection
|
||||
pub connection_id: ConnectionId,
|
||||
|
||||
/// Reference to the connection's local data
|
||||
pub local_data: &'a mut D,
|
||||
}
|
||||
|
@ -1,59 +1,36 @@
|
||||
use std::future::Future;
|
||||
use std::net::IpAddr;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use tokio::task::JoinError;
|
||||
|
||||
use super::ServerRef;
|
||||
|
||||
/// Reference to a TCP server instance.
|
||||
/// Reference to a TCP server instance
|
||||
pub struct TcpServerRef {
|
||||
pub(crate) addr: IpAddr,
|
||||
pub(crate) port: u16,
|
||||
pub(crate) inner: ServerRef,
|
||||
pub(crate) inner: Box<dyn ServerRef>,
|
||||
}
|
||||
|
||||
impl TcpServerRef {
|
||||
pub fn new(addr: IpAddr, port: u16, inner: ServerRef) -> Self {
|
||||
pub fn new(addr: IpAddr, port: u16, inner: Box<dyn ServerRef>) -> Self {
|
||||
Self { addr, port, inner }
|
||||
}
|
||||
|
||||
/// Returns the IP address that the listener is bound to.
|
||||
/// Returns the IP address that the listener is bound to
|
||||
pub fn ip_addr(&self) -> IpAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Returns the port that the listener is bound to.
|
||||
/// Returns the port that the listener is bound to
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// Consumes ref, returning inner ref.
|
||||
pub fn into_inner(self) -> ServerRef {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for TcpServerRef {
|
||||
type Output = Result<(), JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner.task).poll(cx)
|
||||
impl ServerRef for TcpServerRef {
|
||||
fn is_finished(&self) -> bool {
|
||||
self.inner.is_finished()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TcpServerRef {
|
||||
type Target = ServerRef;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for TcpServerRef {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,35 @@
|
||||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use tokio::task::JoinError;
|
||||
|
||||
use super::ServerRef;
|
||||
|
||||
/// Reference to a unix socket server instance.
|
||||
/// Reference to a unix socket server instance
|
||||
pub struct UnixSocketServerRef {
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) inner: ServerRef,
|
||||
pub(crate) inner: Box<dyn ServerRef>,
|
||||
}
|
||||
|
||||
impl UnixSocketServerRef {
|
||||
pub fn new(path: PathBuf, inner: ServerRef) -> Self {
|
||||
pub fn new(path: PathBuf, inner: Box<dyn ServerRef>) -> Self {
|
||||
Self { path, inner }
|
||||
}
|
||||
|
||||
/// Returns the path to the socket.
|
||||
/// Returns the path to the socket
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Consumes ref, returning inner ref.
|
||||
pub fn into_inner(self) -> ServerRef {
|
||||
/// Consumes ref, returning inner ref
|
||||
pub fn into_inner(self) -> Box<dyn ServerRef> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for UnixSocketServerRef {
|
||||
type Output = Result<(), JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner.task).poll(cx)
|
||||
impl ServerRef for UnixSocketServerRef {
|
||||
fn is_finished(&self) -> bool {
|
||||
self.inner.is_finished()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for UnixSocketServerRef {
|
||||
type Target = ServerRef;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for UnixSocketServerRef {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,35 @@
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use tokio::task::JoinError;
|
||||
|
||||
use super::ServerRef;
|
||||
|
||||
/// Reference to a windows pipe server instance.
|
||||
/// Reference to a unix socket server instance
|
||||
pub struct WindowsPipeServerRef {
|
||||
pub(crate) addr: OsString,
|
||||
pub(crate) inner: ServerRef,
|
||||
pub(crate) inner: Box<dyn ServerRef>,
|
||||
}
|
||||
|
||||
impl WindowsPipeServerRef {
|
||||
pub fn new(addr: OsString, inner: ServerRef) -> Self {
|
||||
pub fn new(addr: OsString, inner: Box<dyn ServerRef>) -> Self {
|
||||
Self { addr, inner }
|
||||
}
|
||||
|
||||
/// Returns the addr that the listener is bound to.
|
||||
/// Returns the addr that the listener is bound to
|
||||
pub fn addr(&self) -> &OsStr {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Consumes ref, returning inner ref.
|
||||
pub fn into_inner(self) -> ServerRef {
|
||||
/// Consumes ref, returning inner ref
|
||||
pub fn into_inner(self) -> Box<dyn ServerRef> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for WindowsPipeServerRef {
|
||||
type Output = Result<(), JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner.task).poll(cx)
|
||||
impl ServerRef for WindowsPipeServerRef {
|
||||
fn is_finished(&self) -> bool {
|
||||
self.inner.is_finished()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WindowsPipeServerRef {
|
||||
type Target = ServerRef;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for WindowsPipeServerRef {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -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,6 +1,8 @@
|
||||
mod buf;
|
||||
mod format;
|
||||
mod link;
|
||||
pub mod stdin;
|
||||
|
||||
pub use buf::*;
|
||||
pub use format::*;
|
||||
pub use link::*;
|
||||
|
@ -0,0 +1,404 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use distant_core::net::common::Response;
|
||||
use distant_core::protocol::{
|
||||
self, ChangeKind, Error, FileType, Metadata, SearchQueryContentsMatch, SearchQueryMatch,
|
||||
SearchQueryPathMatch, SystemInfo,
|
||||
};
|
||||
use log::*;
|
||||
use tabled::settings::object::Rows;
|
||||
use tabled::settings::style::Style;
|
||||
use tabled::settings::{Alignment, Disable, Modify};
|
||||
use tabled::{Table, Tabled};
|
||||
|
||||
use crate::options::Format;
|
||||
|
||||
#[derive(Default)]
|
||||
struct FormatterState {
|
||||
/// Last seen path during search
|
||||
pub last_searched_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct Formatter {
|
||||
format: Format,
|
||||
state: FormatterState,
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
/// Create a new output message for the given response based on the specified format
|
||||
pub fn new(format: Format) -> Self {
|
||||
Self {
|
||||
format,
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Formatter`] using [`Format`] of `Format::Shell`
|
||||
pub fn shell() -> Self {
|
||||
Self::new(Format::Shell)
|
||||
}
|
||||
|
||||
/// Consumes the output message, printing it based on its configuration
|
||||
pub fn print(&mut self, res: Response<protocol::Msg<protocol::Response>>) -> io::Result<()> {
|
||||
let output = match self.format {
|
||||
Format::Json => Output::StdoutLine(
|
||||
serde_json::to_vec(&res)
|
||||
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?,
|
||||
),
|
||||
|
||||
// NOTE: For shell, we assume a singular entry in the response's payload
|
||||
Format::Shell if res.payload.is_batch() => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Shell does not support batch responses",
|
||||
))
|
||||
}
|
||||
Format::Shell => format_shell(&mut self.state, res.payload.into_single().unwrap()),
|
||||
};
|
||||
|
||||
match output {
|
||||
Output::Stdout(x) => {
|
||||
// NOTE: Because we are not including a newline in the output,
|
||||
// it is not guaranteed to be written out. In the case of
|
||||
// LSP protocol, the JSON content is not followed by a
|
||||
// newline and was not picked up when the response was
|
||||
// sent back to the client; so, we need to manually flush
|
||||
if let Err(x) = io::stdout().lock().write_all(&x) {
|
||||
error!("Failed to write stdout: {}", x);
|
||||
}
|
||||
|
||||
if let Err(x) = io::stdout().lock().flush() {
|
||||
error!("Failed to flush stdout: {}", x);
|
||||
}
|
||||
}
|
||||
Output::StdoutLine(x) => {
|
||||
if let Err(x) = io::stdout().lock().write_all(&x) {
|
||||
error!("Failed to write stdout: {}", x);
|
||||
}
|
||||
|
||||
if let Err(x) = io::stdout().lock().write(b"\n") {
|
||||
error!("Failed to write stdout newline: {}", x);
|
||||
}
|
||||
}
|
||||
Output::Stderr(x) => {
|
||||
// NOTE: Because we are not including a newline in the output,
|
||||
// it is not guaranteed to be written out. In the case of
|
||||
// LSP protocol, the JSON content is not followed by a
|
||||
// newline and was not picked up when the response was
|
||||
// sent back to the client; so, we need to manually flush
|
||||
if let Err(x) = io::stderr().lock().write_all(&x) {
|
||||
error!("Failed to write stderr: {}", x);
|
||||
}
|
||||
|
||||
if let Err(x) = io::stderr().lock().flush() {
|
||||
error!("Failed to flush stderr: {}", x);
|
||||
}
|
||||
}
|
||||
Output::StderrLine(x) => {
|
||||
if let Err(x) = io::stderr().lock().write_all(&x) {
|
||||
error!("Failed to write stderr: {}", x);
|
||||
}
|
||||
|
||||
if let Err(x) = io::stderr().lock().write(b"\n") {
|
||||
error!("Failed to write stderr newline: {}", x);
|
||||
}
|
||||
}
|
||||
Output::None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the output content and destination
|
||||
enum Output {
|
||||
Stdout(Vec<u8>),
|
||||
StdoutLine(Vec<u8>),
|
||||
Stderr(Vec<u8>),
|
||||
StderrLine(Vec<u8>),
|
||||
None,
|
||||
}
|
||||
|
||||
fn format_shell(state: &mut FormatterState, data: protocol::Response) -> Output {
|
||||
match data {
|
||||
protocol::Response::Ok => Output::None,
|
||||
protocol::Response::Error(Error { description, .. }) => {
|
||||
Output::StderrLine(description.into_bytes())
|
||||
}
|
||||
protocol::Response::Blob { data } => Output::StdoutLine(data),
|
||||
protocol::Response::Text { data } => Output::StdoutLine(data.into_bytes()),
|
||||
protocol::Response::DirEntries { entries, .. } => {
|
||||
#[derive(Tabled)]
|
||||
struct EntryRow {
|
||||
ty: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
let table = Table::new(entries.into_iter().map(|entry| EntryRow {
|
||||
ty: String::from(match entry.file_type {
|
||||
FileType::Dir => "<DIR>",
|
||||
FileType::File => "",
|
||||
FileType::Symlink => "<SYMLINK>",
|
||||
}),
|
||||
path: entry.path.to_string_lossy().to_string(),
|
||||
}))
|
||||
.with(Style::blank())
|
||||
.with(Disable::row(Rows::new(..1)))
|
||||
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
|
||||
.to_string()
|
||||
.into_bytes();
|
||||
|
||||
Output::Stdout(table)
|
||||
}
|
||||
protocol::Response::Changed(change) => Output::StdoutLine(
|
||||
format!(
|
||||
"{}{}",
|
||||
match change.kind {
|
||||
ChangeKind::Create => "Following paths were created:\n",
|
||||
ChangeKind::Delete => "Following paths were removed:\n",
|
||||
x if x.is_access() => "Following paths were accessed:\n",
|
||||
x if x.is_modify() => "Following paths were modified:\n",
|
||||
x if x.is_rename() => "Following paths were renamed:\n",
|
||||
_ => "Following paths were affected:\n",
|
||||
},
|
||||
change
|
||||
.paths
|
||||
.into_iter()
|
||||
.map(|p| format!("* {}", p.to_string_lossy()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
)
|
||||
.into_bytes(),
|
||||
),
|
||||
protocol::Response::Exists { value: exists } => {
|
||||
if exists {
|
||||
Output::StdoutLine(b"true".to_vec())
|
||||
} else {
|
||||
Output::StdoutLine(b"false".to_vec())
|
||||
}
|
||||
}
|
||||
protocol::Response::Metadata(Metadata {
|
||||
canonicalized_path,
|
||||
file_type,
|
||||
len,
|
||||
readonly,
|
||||
accessed,
|
||||
created,
|
||||
modified,
|
||||
unix,
|
||||
windows,
|
||||
}) => Output::StdoutLine(
|
||||
format!(
|
||||
concat!(
|
||||
"{}",
|
||||
"Type: {}\n",
|
||||
"Len: {}\n",
|
||||
"Readonly: {}\n",
|
||||
"Created: {}\n",
|
||||
"Last Accessed: {}\n",
|
||||
"Last Modified: {}\n",
|
||||
"{}",
|
||||
"{}",
|
||||
"{}",
|
||||
),
|
||||
canonicalized_path
|
||||
.map(|p| format!("Canonicalized Path: {p:?}\n"))
|
||||
.unwrap_or_default(),
|
||||
file_type.as_ref(),
|
||||
len,
|
||||
readonly,
|
||||
created.unwrap_or_default(),
|
||||
accessed.unwrap_or_default(),
|
||||
modified.unwrap_or_default(),
|
||||
unix.map(|u| format!(
|
||||
concat!(
|
||||
"Owner Read: {}\n",
|
||||
"Owner Write: {}\n",
|
||||
"Owner Exec: {}\n",
|
||||
"Group Read: {}\n",
|
||||
"Group Write: {}\n",
|
||||
"Group Exec: {}\n",
|
||||
"Other Read: {}\n",
|
||||
"Other Write: {}\n",
|
||||
"Other Exec: {}",
|
||||
),
|
||||
u.owner_read,
|
||||
u.owner_write,
|
||||
u.owner_exec,
|
||||
u.group_read,
|
||||
u.group_write,
|
||||
u.group_exec,
|
||||
u.other_read,
|
||||
u.other_write,
|
||||
u.other_exec
|
||||
))
|
||||
.unwrap_or_default(),
|
||||
windows
|
||||
.map(|w| format!(
|
||||
concat!(
|
||||
"Archive: {}\n",
|
||||
"Compressed: {}\n",
|
||||
"Encrypted: {}\n",
|
||||
"Hidden: {}\n",
|
||||
"Integrity Stream: {}\n",
|
||||
"Normal: {}\n",
|
||||
"Not Content Indexed: {}\n",
|
||||
"No Scrub Data: {}\n",
|
||||
"Offline: {}\n",
|
||||
"Recall on Data Access: {}\n",
|
||||
"Recall on Open: {}\n",
|
||||
"Reparse Point: {}\n",
|
||||
"Sparse File: {}\n",
|
||||
"System: {}\n",
|
||||
"Temporary: {}",
|
||||
),
|
||||
w.archive,
|
||||
w.compressed,
|
||||
w.encrypted,
|
||||
w.hidden,
|
||||
w.integrity_stream,
|
||||
w.normal,
|
||||
w.not_content_indexed,
|
||||
w.no_scrub_data,
|
||||
w.offline,
|
||||
w.recall_on_data_access,
|
||||
w.recall_on_open,
|
||||
w.reparse_point,
|
||||
w.sparse_file,
|
||||
w.system,
|
||||
w.temporary,
|
||||
))
|
||||
.unwrap_or_default(),
|
||||
if unix.is_none() && windows.is_none() {
|
||||
String::from("\n")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)
|
||||
.into_bytes(),
|
||||
),
|
||||
protocol::Response::SearchStarted { id } => {
|
||||
Output::StdoutLine(format!("Query {id} started").into_bytes())
|
||||
}
|
||||
protocol::Response::SearchDone { .. } => Output::None,
|
||||
protocol::Response::SearchResults { matches, .. } => {
|
||||
let mut files: HashMap<_, Vec<String>> = HashMap::new();
|
||||
let mut is_targeting_paths = false;
|
||||
|
||||
for m in matches {
|
||||
match m {
|
||||
SearchQueryMatch::Path(SearchQueryPathMatch { path, .. }) => {
|
||||
// Create the entry with no lines called out
|
||||
files.entry(path).or_default();
|
||||
is_targeting_paths = true;
|
||||
}
|
||||
|
||||
SearchQueryMatch::Contents(SearchQueryContentsMatch {
|
||||
path,
|
||||
lines,
|
||||
line_number,
|
||||
..
|
||||
}) => {
|
||||
let file_matches = files.entry(path).or_default();
|
||||
|
||||
file_matches.push(format!(
|
||||
"{line_number}:{}",
|
||||
lines.to_string_lossy().trim_end()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
for (path, lines) in files {
|
||||
use std::fmt::Write;
|
||||
|
||||
// If we are seening a new path, print it out
|
||||
if state.last_searched_path.as_deref() != Some(path.as_path()) {
|
||||
// If we have already seen some path before, we would have printed it, and
|
||||
// we want to add a space between it and the current path, but only if we are
|
||||
// printing out file content matches and not paths
|
||||
if state.last_searched_path.is_some() && !is_targeting_paths {
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut output, "{}", path.to_string_lossy()).unwrap();
|
||||
}
|
||||
|
||||
for line in lines {
|
||||
writeln!(&mut output, "{line}").unwrap();
|
||||
}
|
||||
|
||||
// Update our last seen path
|
||||
state.last_searched_path = Some(path);
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Output::Stdout(output.into_bytes())
|
||||
} else {
|
||||
Output::None
|
||||
}
|
||||
}
|
||||
protocol::Response::ProcSpawned { .. } => Output::None,
|
||||
protocol::Response::ProcStdout { data, .. } => Output::Stdout(data),
|
||||
protocol::Response::ProcStderr { data, .. } => Output::Stderr(data),
|
||||
protocol::Response::ProcDone { id, success, code } => {
|
||||
if success {
|
||||
Output::None
|
||||
} else if let Some(code) = code {
|
||||
Output::StderrLine(format!("Proc {id} failed with code {code}").into_bytes())
|
||||
} else {
|
||||
Output::StderrLine(format!("Proc {id} failed").into_bytes())
|
||||
}
|
||||
}
|
||||
protocol::Response::SystemInfo(SystemInfo {
|
||||
family,
|
||||
os,
|
||||
arch,
|
||||
current_dir,
|
||||
main_separator,
|
||||
username,
|
||||
shell,
|
||||
}) => Output::StdoutLine(
|
||||
format!(
|
||||
concat!(
|
||||
"Family: {:?}\n",
|
||||
"Operating System: {:?}\n",
|
||||
"Arch: {:?}\n",
|
||||
"Cwd: {:?}\n",
|
||||
"Path Sep: {:?}\n",
|
||||
"Username: {:?}\n",
|
||||
"Shell: {:?}"
|
||||
),
|
||||
family, os, arch, current_dir, main_separator, username, shell
|
||||
)
|
||||
.into_bytes(),
|
||||
),
|
||||
protocol::Response::Version(version) => {
|
||||
#[derive(Tabled)]
|
||||
struct EntryRow {
|
||||
kind: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
let table = Table::new(
|
||||
version
|
||||
.capabilities
|
||||
.into_sorted_vec()
|
||||
.into_iter()
|
||||
.map(|cap| EntryRow {
|
||||
kind: cap.kind,
|
||||
description: cap.description,
|
||||
}),
|
||||
)
|
||||
.with(Style::ascii())
|
||||
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
|
||||
.to_string()
|
||||
.into_bytes();
|
||||
|
||||
Output::StdoutLine(table)
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue