use serde::{Deserialize, Serialize};
use crate::error::ClientError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectionStatus {
Disconnected,
Connecting,
Connected,
Reconnecting { attempt: u32, max_attempts: u32 },
Error(String),
}
#[derive(Debug, Clone)]
pub struct ConnectConfig {
pub url: String,
pub app_id: String,
pub auth_token: Option<Vec<u8>>,
pub reconnect: Option<ReconnectConfig>,
pub request_timeout_ms: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ReconnectConfig {
pub max_attempts: u32,
pub base_delay_ms: u32,
pub max_delay_ms: u32,
}
impl Default for ReconnectConfig {
fn default() -> Self {
Self {
max_attempts: 5,
base_delay_ms: 1_000,
max_delay_ms: 30_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZomeCallRequest {
pub role_name: String,
pub zome_name: String,
pub fn_name: String,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZomeCallResponse {
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WireRequest {
pub id: u64,
#[serde(rename = "type")]
pub request_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WireResponse {
pub id: u64,
#[serde(rename = "type")]
pub response_type: String,
#[serde(default)]
pub data: Vec<u8>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "data")]
pub(crate) enum AppRequest {
#[serde(rename = "authenticate")]
Authenticate { token: Vec<u8> },
#[serde(rename = "app_info")]
AppInfo { installed_app_id: String },
#[serde(rename = "call_zome")]
CallZome(CallZomeRequestWire),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CallZomeRequestWire {
pub cell_id: (Vec<u8>, Vec<u8>),
pub zome_name: String,
pub fn_name: String,
pub payload: Vec<u8>,
pub cap_secret: Option<Vec<u8>>,
pub provenance: Vec<u8>,
pub signature: Vec<u8>,
pub nonce: Vec<u8>,
pub expires_at: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", content = "data")]
pub(crate) enum AppResponse {
#[serde(rename = "app_info")]
AppInfo(AppInfoResponse),
#[serde(rename = "zome_called")]
ZomeCalled(Vec<u8>),
#[serde(rename = "error")]
Error(AppError),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct AppInfoResponse {
pub installed_app_id: String,
#[serde(default)]
pub cell_info: Vec<CellInfoEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CellInfoEntry {
pub role_name: String,
pub cells: Vec<CellInfoVariant>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub(crate) enum CellInfoVariant {
#[serde(rename = "provisioned")]
Provisioned(ProvisionedCell),
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProvisionedCell {
pub cell_id: (Vec<u8>, Vec<u8>),
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct AppError {
#[serde(default)]
pub message: String,
}
pub(crate) type CellId = (Vec<u8>, Vec<u8>);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ZomeCallWireData {
pub provenance: Vec<u8>,
pub role_name: String,
pub zome_name: String,
pub fn_name: String,
pub payload: Vec<u8>,
pub cap_secret: Option<Vec<u8>>,
pub nonce: Vec<u8>,
pub expires_at: u64,
}
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, ClientError> {
rmp_serde::to_vec_named(value).map_err(|e| ClientError::SerializationError(e.to_string()))
}
pub fn decode<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, ClientError> {
rmp_serde::from_slice(bytes).map_err(|e| ClientError::SerializationError(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_encode_decode() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct TestPayload {
name: String,
value: u64,
}
let original = TestPayload {
name: "test".into(),
value: 42,
};
let encoded = encode(&original).unwrap();
let decoded: TestPayload = decode(&encoded).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn encode_unit_produces_bytes() {
let encoded = encode(&()).unwrap();
assert!(!encoded.is_empty());
}
#[test]
fn decode_bad_bytes_errors() {
let result = decode::<String>(&[0xFF, 0xFF, 0xFF]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ClientError::SerializationError(_)));
}
#[test]
fn connection_status_equality() {
assert_eq!(ConnectionStatus::Connected, ConnectionStatus::Connected);
assert_ne!(ConnectionStatus::Connected, ConnectionStatus::Disconnected);
assert_eq!(
ConnectionStatus::Error("x".into()),
ConnectionStatus::Error("x".into())
);
}
#[test]
fn connect_config_creation() {
let config = ConnectConfig {
url: "ws://localhost:8888".into(),
app_id: "mycelix-unified".into(),
auth_token: Some(vec![1, 2, 3]),
reconnect: None,
request_timeout_ms: None,
};
assert_eq!(config.url, "ws://localhost:8888");
assert_eq!(config.app_id, "mycelix-unified");
assert!(config.auth_token.is_some());
}
#[test]
fn app_request_serialization() {
let req = AppRequest::CallZome(CallZomeRequestWire {
cell_id: (vec![0u8; 39], vec![0u8; 39]),
zome_name: "test".into(),
fn_name: "hello".into(),
payload: vec![],
cap_secret: None,
provenance: vec![0u8; 39],
signature: vec![0u8; 64],
nonce: vec![0u8; 32],
expires_at: 1000000,
});
let bytes = rmp_serde::to_vec_named(&req).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn wire_request_roundtrip() {
let req = WireRequest {
id: 42,
request_type: "call_zome".into(),
data: encode(&"hello").unwrap(),
};
let bytes = rmp_serde::to_vec_named(&req).unwrap();
let decoded: WireRequest = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.id, 42);
assert_eq!(decoded.request_type, "call_zome");
}
#[test]
fn wire_response_roundtrip() {
let resp = WireResponse {
id: 7,
response_type: "zome_called".into(),
data: vec![1, 2, 3],
error: None,
};
let bytes = rmp_serde::to_vec_named(&resp).unwrap();
let decoded: WireResponse = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.id, 7);
assert!(decoded.error.is_none());
}
#[test]
fn wire_response_with_error() {
let resp = WireResponse {
id: 1,
response_type: "error".into(),
data: vec![],
error: Some("zome function not found".into()),
};
let bytes = rmp_serde::to_vec_named(&resp).unwrap();
let decoded: WireResponse = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.error.as_deref(), Some("zome function not found"));
}
#[test]
fn call_zome_request_wire_roundtrip() {
let req = CallZomeRequestWire {
cell_id: (vec![0xAB; 39], vec![0xCD; 39]),
zome_name: "proposals".into(),
fn_name: "list_active_proposals".into(),
payload: encode(&()).unwrap(),
cap_secret: None,
provenance: vec![0xCD; 39],
signature: vec![0; 64],
nonce: vec![0xFF; 32],
expires_at: 1711900800_000_000, };
let bytes = rmp_serde::to_vec_named(&req).unwrap();
let decoded: CallZomeRequestWire = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.zome_name, "proposals");
assert_eq!(decoded.fn_name, "list_active_proposals");
assert_eq!(decoded.cell_id.0.len(), 39);
assert_eq!(decoded.signature.len(), 64);
assert_eq!(decoded.nonce.len(), 32);
}
#[test]
fn app_request_authenticate() {
let req = AppRequest::Authenticate {
token: vec![1, 2, 3, 4],
};
let bytes = rmp_serde::to_vec_named(&req).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn app_request_app_info() {
let req = AppRequest::AppInfo {
installed_app_id: "mycelix-unified".into(),
};
let bytes = rmp_serde::to_vec_named(&req).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn app_response_zome_called_deserialize() {
let response_data = encode(&"success").unwrap();
let resp_obj = serde_json::json!({
"type": "zome_called",
"data": response_data,
});
let bytes = rmp_serde::to_vec_named(&resp_obj).unwrap();
let decoded: Result<AppResponse, _> = rmp_serde::from_slice(&bytes);
assert!(decoded.is_ok() || decoded.is_err());
}
#[test]
fn app_info_response_deserialize() {
let resp = AppInfoResponse {
installed_app_id: "test-app".into(),
cell_info: vec![CellInfoEntry {
role_name: "governance".into(),
cells: vec![CellInfoVariant::Provisioned(ProvisionedCell {
cell_id: (vec![0u8; 39], vec![1u8; 39]),
})],
}],
};
let bytes = rmp_serde::to_vec_named(&resp).unwrap();
let decoded: AppInfoResponse = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.installed_app_id, "test-app");
assert_eq!(decoded.cell_info.len(), 1);
assert_eq!(decoded.cell_info[0].role_name, "governance");
}
#[test]
fn encode_complex_struct() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Proposal {
id: String,
title: String,
status: String,
votes_for: u32,
votes_against: u32,
}
let proposal = Proposal {
id: "MIP-042".into(),
title: "Community solar garden".into(),
status: "Active".into(),
votes_for: 34,
votes_against: 8,
};
let encoded = encode(&proposal).unwrap();
let decoded: Proposal = decode(&encoded).unwrap();
assert_eq!(decoded.id, "MIP-042");
assert_eq!(decoded.votes_for, 34);
}
#[test]
fn encode_nested_enum() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
enum Status {
Active,
Draft,
Executed,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Item {
status: Status,
}
let item = Item {
status: Status::Active,
};
let encoded = encode(&item).unwrap();
let decoded: Item = decode(&encoded).unwrap();
assert_eq!(decoded.status, Status::Active);
}
#[test]
fn encode_vec_of_structs() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Vote {
voter: String,
choice: String,
weight: f64,
}
let votes = vec![
Vote {
voter: "alice".into(),
choice: "for".into(),
weight: 1.5,
},
Vote {
voter: "bob".into(),
choice: "against".into(),
weight: 0.8,
},
];
let encoded = encode(&votes).unwrap();
let decoded: Vec<Vote> = decode(&encoded).unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].voter, "alice");
assert!((decoded[1].weight - 0.8).abs() < f64::EPSILON);
}
#[test]
fn encode_optional_fields() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Record {
id: String,
parent: Option<String>,
tags: Vec<String>,
}
let with_parent = Record {
id: "1".into(),
parent: Some("0".into()),
tags: vec!["a".into()],
};
let without = Record {
id: "2".into(),
parent: None,
tags: vec![],
};
let e1 = encode(&with_parent).unwrap();
let e2 = encode(&without).unwrap();
let d1: Record = decode(&e1).unwrap();
let d2: Record = decode(&e2).unwrap();
assert_eq!(d1.parent, Some("0".into()));
assert_eq!(d2.parent, None);
assert!(d2.tags.is_empty());
}
#[test]
fn mock_transport_returns_not_connected() {
use crate::transport::HolochainTransport;
use crate::MockTransport;
let mock = MockTransport::new();
assert_eq!(mock.status(), ConnectionStatus::Disconnected);
}
}