use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Request {
pub req_id: Uuid,
#[serde(flatten)]
pub op: Op,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Op {
NodeId,
Import {
path: PathBuf,
rings: Vec<String>,
open: bool,
},
BlobList {
peer: Option<String>,
rings: Option<Vec<String>>,
},
BlobRemove {
target: String,
},
Tag {
target: String,
rings: Vec<String>,
open: bool,
},
Untag {
target: String,
rings: Vec<String>,
open: bool,
all: bool,
},
RingNew {
name: String,
},
RingList,
RingAdd {
ring: String,
peer: String,
},
RingRemove {
ring: String,
peer: String,
},
RingMembers {
ring: String,
},
Receive {
ticket: String,
dest: PathBuf,
force_overwrite: bool,
},
Grant {
peer: String,
privilege: String,
},
Revoke {
peer: String,
privilege: String,
},
Grants {
#[serde(default, skip_serializing_if = "Option::is_none")]
peer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
privilege: Option<String>,
},
RemoteBlobList {
peer: String,
},
PeerAdd {
peer: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
},
PeerList,
PeerRemove {
peer: String,
},
Shutdown,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub req_id: Uuid,
#[serde(flatten)]
pub kind: EventKind,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventKind {
Line {
text: String,
},
Progress {
done: u64,
total: u64,
},
Done,
Error {
message: String,
},
Record {
value: serde_json::Value,
},
}
impl Event {
pub fn line(req_id: Uuid, text: impl Into<String>) -> Self {
Self {
req_id,
kind: EventKind::Line { text: text.into() },
}
}
pub fn blank(req_id: Uuid) -> Self {
Self::line(req_id, "")
}
pub fn progress(req_id: Uuid, done: u64, total: u64) -> Self {
Self {
req_id,
kind: EventKind::Progress { done, total },
}
}
pub fn done(req_id: Uuid) -> Self {
Self {
req_id,
kind: EventKind::Done,
}
}
pub fn error(req_id: Uuid, message: impl Into<String>) -> Self {
Self {
req_id,
kind: EventKind::Error {
message: message.into(),
},
}
}
pub fn record(req_id: Uuid, value: serde_json::Value) -> Self {
Self {
req_id,
kind: EventKind::Record { value },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_node_id_serializes_to_snake_case_tag() {
assert_eq!(
serde_json::to_string(&Op::NodeId).unwrap(),
r#"{"op":"node_id"}"#
);
}
#[test]
fn op_blob_list_serializes_correctly() {
assert_eq!(
serde_json::to_string(&Op::BlobList {
peer: None,
rings: None,
})
.unwrap(),
r#"{"op":"blob_list","peer":null,"rings":null}"#
);
}
#[test]
fn op_ring_new_serializes_with_name_field() {
let json = serde_json::to_string(&Op::RingNew {
name: "friends".into(),
})
.unwrap();
assert_eq!(json, r#"{"op":"ring_new","name":"friends"}"#);
}
#[test]
fn request_serializes_req_id() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let req = Request {
req_id: id,
op: Op::BlobList {
peer: None,
rings: None,
},
};
assert_eq!(
serde_json::to_string(&req).unwrap(),
r#"{"req_id":"550e8400-e29b-41d4-a716-446655440000","op":"blob_list","peer":null,"rings":null}"#
);
}
#[test]
fn request_without_req_id_fails_to_deserialize() {
let result: Result<Request, _> = serde_json::from_str(r#"{"op":"node_id"}"#);
assert!(result.is_err());
}
#[test]
fn request_with_empty_req_id_fails_to_deserialize() {
let result: Result<Request, _> = serde_json::from_str(r#"{"req_id":"","op":"node_id"}"#);
assert!(result.is_err());
}
#[test]
fn request_with_malformed_req_id_fails_to_deserialize() {
let result: Result<Request, _> =
serde_json::from_str(r#"{"req_id":"not-a-uuid","op":"node_id"}"#);
assert!(result.is_err());
}
#[test]
fn event_done_serializes_req_id_and_type() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(
serde_json::to_string(&Event::done(id)).unwrap(),
r#"{"req_id":"550e8400-e29b-41d4-a716-446655440000","type":"done"}"#
);
}
#[test]
fn event_line_serializes_req_id_type_and_text() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(
serde_json::to_string(&Event::line(id, "hello world")).unwrap(),
r#"{"req_id":"550e8400-e29b-41d4-a716-446655440000","type":"line","text":"hello world"}"#
);
}
#[test]
fn event_progress_serializes_correctly() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(
serde_json::to_string(&Event::progress(id, 50, 100)).unwrap(),
r#"{"req_id":"550e8400-e29b-41d4-a716-446655440000","type":"progress","done":50,"total":100}"#
);
}
#[test]
fn event_error_serializes_correctly() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(
serde_json::to_string(&Event::error(id, "something went wrong")).unwrap(),
r#"{"req_id":"550e8400-e29b-41d4-a716-446655440000","type":"error","message":"something went wrong"}"#
);
}
#[test]
fn event_without_req_id_fails_to_deserialize() {
let result: Result<Event, _> = serde_json::from_str(r#"{"type":"done"}"#);
assert!(result.is_err());
}
#[test]
fn request_round_trips_through_json() {
let original = Request {
req_id: Uuid::new_v4(),
op: Op::RingNew {
name: "work".into(),
},
};
let parsed: Request =
serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn event_round_trips_through_json() {
let original = Event::progress(Uuid::new_v4(), 42, 100);
let parsed: Event =
serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn op_grant_serializes_correctly() {
let json = serde_json::to_string(&Op::Grant {
peer: "abc123".into(),
privilege: "blob-list".into(),
})
.unwrap();
assert_eq!(
json,
r#"{"op":"grant","peer":"abc123","privilege":"blob-list"}"#
);
}
#[test]
fn op_revoke_serializes_correctly() {
let json = serde_json::to_string(&Op::Revoke {
peer: "abc123".into(),
privilege: "blob-list".into(),
})
.unwrap();
assert_eq!(
json,
r#"{"op":"revoke","peer":"abc123","privilege":"blob-list"}"#
);
}
#[test]
fn op_grants_without_filters_serializes_to_op_only() {
assert_eq!(
serde_json::to_string(&Op::Grants {
peer: None,
privilege: None,
})
.unwrap(),
r#"{"op":"grants"}"#
);
}
#[test]
fn op_grants_with_filters_serializes_optional_fields() {
let json = serde_json::to_string(&Op::Grants {
peer: Some("abc123".into()),
privilege: Some("blob-list".into()),
})
.unwrap();
assert_eq!(
json,
r#"{"op":"grants","peer":"abc123","privilege":"blob-list"}"#
);
}
#[test]
fn op_remote_blob_list_serializes_correctly() {
let json = serde_json::to_string(&Op::RemoteBlobList {
peer: "abc123".into(),
})
.unwrap();
assert_eq!(json, r#"{"op":"remote_blob_list","peer":"abc123"}"#);
}
#[test]
fn op_grants_without_filters_deserializes_correctly() {
let op: Op = serde_json::from_str(r#"{"op":"grants"}"#).unwrap();
assert_eq!(
op,
Op::Grants {
peer: None,
privilege: None
}
);
}
#[test]
fn op_grants_with_peer_filter_deserializes_correctly() {
let op: Op = serde_json::from_str(r#"{"op":"grants","peer":"abc123"}"#).unwrap();
assert_eq!(
op,
Op::Grants {
peer: Some("abc123".into()),
privilege: None
}
);
}
#[test]
fn op_grants_with_both_filters_round_trips() {
let original = Op::Grants {
peer: Some("abc123".into()),
privilege: Some("blob-list".into()),
};
let parsed: Op = serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn op_ring_add_serializes_without_nickname_field() {
let json = serde_json::to_string(&Op::RingAdd {
ring: "friends".into(),
peer: "abc123".into(),
})
.unwrap();
assert_eq!(
json,
r#"{"op":"ring_add","ring":"friends","peer":"abc123"}"#
);
}
#[test]
fn op_untag_with_all_serializes_correctly() {
let json = serde_json::to_string(&Op::Untag {
target: "abc.txt".into(),
rings: vec![],
open: false,
all: true,
})
.unwrap();
assert_eq!(
json,
r#"{"op":"untag","target":"abc.txt","rings":[],"open":false,"all":true}"#
);
}
#[test]
fn op_untag_round_trips_through_json() {
let original = Op::Untag {
target: "abc.txt".into(),
rings: vec!["friends".into()],
open: false,
all: false,
};
let parsed: Op = serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn op_peer_add_without_nickname_omits_nickname_field() {
let json = serde_json::to_string(&Op::PeerAdd {
peer: "abc123".into(),
nickname: None,
})
.unwrap();
assert_eq!(json, r#"{"op":"peer_add","peer":"abc123"}"#);
}
#[test]
fn op_peer_add_with_nickname_includes_nickname_field() {
let json = serde_json::to_string(&Op::PeerAdd {
peer: "abc123".into(),
nickname: Some("alice".into()),
})
.unwrap();
assert_eq!(
json,
r#"{"op":"peer_add","peer":"abc123","nickname":"alice"}"#
);
}
#[test]
fn op_peer_list_serializes_correctly() {
assert_eq!(
serde_json::to_string(&Op::PeerList).unwrap(),
r#"{"op":"peer_list"}"#
);
}
#[test]
fn op_peer_remove_serializes_correctly() {
let json = serde_json::to_string(&Op::PeerRemove {
peer: "abc123".into(),
})
.unwrap();
assert_eq!(json, r#"{"op":"peer_remove","peer":"abc123"}"#);
}
#[test]
fn op_peer_add_round_trips_with_nickname() {
let original = Op::PeerAdd {
peer: "abc123".into(),
nickname: Some("alice".into()),
};
let parsed: Op = serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn op_peer_add_without_nickname_deserializes_correctly() {
let op: Op = serde_json::from_str(r#"{"op":"peer_add","peer":"abc123"}"#).unwrap();
assert_eq!(
op,
Op::PeerAdd {
peer: "abc123".into(),
nickname: None,
}
);
}
#[test]
fn event_record_serializes_correctly() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let value = serde_json::json!({"hash": "abc123", "name": "file.txt"});
let event = Event::record(id, value);
let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["type"], "record");
assert_eq!(parsed["value"]["hash"], "abc123");
assert_eq!(parsed["value"]["name"], "file.txt");
assert_eq!(parsed["req_id"], "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn event_record_round_trips_through_json() {
let original = Event::record(
Uuid::new_v4(),
serde_json::json!({"peer_id": "deadbeef", "rings": ["friends", "work"]}),
);
let parsed: Event =
serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn event_blank_produces_empty_text() {
let id = Uuid::new_v4();
let event = Event::blank(id);
assert_eq!(
event.kind,
EventKind::Line {
text: String::new()
}
);
}
#[test]
fn event_record_round_trips_with_non_object_json_value() {
let id = Uuid::new_v4();
let original = Event::record(id, serde_json::json!([1, 2, 3]));
let parsed: Event =
serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
assert_eq!(parsed, original);
}
}