#![allow(deprecated)]
use std::collections::HashSet;
use std::io;
use std::time;
use serde::{Deserialize, Serialize};
use serde_json as json;
use crate::crypto::PublicKey;
use crate::identity::RepoId;
use crate::storage::refs;
use super::events::Event;
use super::NodeId;
pub const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(30);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "command")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Command {
#[serde(rename_all = "camelCase")]
#[deprecated(note = "use `AnnounceRefsFor` instead")]
AnnounceRefs { rid: RepoId },
#[serde(rename_all = "camelCase")]
AnnounceRefsFor {
rid: RepoId,
namespaces: HashSet<PublicKey>,
},
#[serde(rename_all = "camelCase")]
AnnounceInventory,
AddInventory { rid: RepoId },
Config,
ListenAddrs,
#[serde(rename_all = "camelCase")]
Connect {
addr: super::config::ConnectAddress,
opts: ConnectOptions,
},
#[serde(rename_all = "camelCase")]
Disconnect { nid: NodeId },
#[serde(rename_all = "camelCase")]
#[deprecated(note = "use `SeedsFor` instead")]
Seeds { rid: RepoId },
#[serde(rename_all = "camelCase")]
SeedsFor {
rid: RepoId,
namespaces: HashSet<PublicKey>,
},
Sessions,
Session { nid: NodeId },
#[serde(rename_all = "camelCase")]
Fetch {
rid: RepoId,
nid: NodeId,
timeout: time::Duration,
signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
},
#[serde(rename_all = "camelCase")]
Seed {
rid: RepoId,
scope: super::policy::Scope,
},
#[serde(rename_all = "camelCase")]
Unseed { rid: RepoId },
#[serde(rename_all = "camelCase")]
Follow {
nid: NodeId,
alias: Option<super::Alias>,
},
#[serde(rename_all = "camelCase")]
Unfollow { nid: NodeId },
#[serde(rename_all = "camelCase")]
Block { nid: NodeId },
Status,
Debug,
NodeId,
Shutdown,
Subscribe,
}
impl Command {
pub fn to_writer(&self, mut w: impl io::Write) -> io::Result<()> {
json::to_writer(&mut w, self).map_err(|_| io::ErrorKind::InvalidInput)?;
w.write_all(b"\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConnectOptions {
pub persistent: bool,
pub timeout: time::Duration,
}
impl Default for ConnectOptions {
fn default() -> Self {
Self {
persistent: false,
timeout: DEFAULT_TIMEOUT,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CommandResult<T> {
Okay(T),
Error {
#[serde(rename = "error")]
reason: String,
},
}
impl<T, E> From<Result<T, E>> for CommandResult<T>
where
E: std::error::Error,
{
fn from(result: Result<T, E>) -> Self {
match result {
Ok(t) => Self::Okay(t),
Err(e) => Self::Error {
reason: e.to_string(),
},
}
}
}
impl From<Event> for CommandResult<Event> {
fn from(event: Event) -> Self {
Self::Okay(event)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Success {
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
pub(super) updated: bool,
}
impl CommandResult<Success> {
pub fn updated(updated: bool) -> Self {
Self::Okay(Success { updated })
}
pub fn ok() -> Self {
Self::Okay(Success { updated: false })
}
}
impl CommandResult<()> {
pub fn error(err: impl std::error::Error) -> Self {
Self::Error {
reason: err.to_string(),
}
}
}
impl<T: Serialize> CommandResult<T> {
pub fn to_writer(&self, mut w: impl io::Write) -> io::Result<()> {
json::to_writer(&mut w, self).map_err(|_| io::ErrorKind::InvalidInput)?;
w.write_all(b"\n")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use std::collections::VecDeque;
use localtime::LocalTime;
use crate::assert_matches;
use crate::node::{Seeds, State};
#[test]
fn command_result() {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Test {
value: u32,
}
assert_eq!(json::to_string(&CommandResult::Okay(true)).unwrap(), "true");
assert_eq!(
json::to_string(&CommandResult::Okay(Test { value: 42 })).unwrap(),
"{\"value\":42}"
);
assert_eq!(
json::from_str::<CommandResult<Test>>("{\"value\":42}").unwrap(),
CommandResult::Okay(Test { value: 42 })
);
assert_eq!(json::to_string(&CommandResult::ok()).unwrap(), "{}");
assert_eq!(
json::to_string(&CommandResult::updated(true)).unwrap(),
"{\"updated\":true}"
);
assert_eq!(
json::to_string(&CommandResult::error(io::Error::from(
io::ErrorKind::NotFound
)))
.unwrap(),
"{\"error\":\"entity not found\"}"
);
json::from_str::<CommandResult<State>>(
&serde_json::to_string(&CommandResult::Okay(State::Connected {
since: LocalTime::now(),
ping: Default::default(),
latencies: VecDeque::default(),
stable: false,
}))
.unwrap(),
)
.unwrap();
assert_matches!(
json::from_str::<CommandResult<State>>(
r#"{"connected":{"since":1699636852107,"fetching":[]}}"#
),
Ok(CommandResult::Okay(_))
);
assert_matches!(
json::from_str::<CommandResult<Seeds>>(
r#"[{"nid":"z6MksmpU5b1dS7oaqF2bHXhQi1DWy2hB7Mh9CuN7y1DN6QSz","addrs":[{"addr":"seed.radicle.example.com:8776","source":"peer","lastSuccess":1699983994234,"lastAttempt":1699983994000,"banned":false}],"state":{"connected":{"since":1699983994}}}]"#
),
Ok(CommandResult::Okay(_))
);
}
}