use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};
use url::Url;
const SYSTEM_PARAMETER_DEPTH_SAFETY_CEILING: usize = 128;
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DittoConfig {
pub database_id: String,
pub connect: DittoConfigConnect,
#[serde(skip_serializing_if = "Option::is_none")]
pub persistence_directory: Option<PathBuf>,
#[doc(hidden)]
#[serde(default)]
pub experimental: DittoConfigExperimental,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub system_parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct InternalConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) legacy_persistence_directory: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ActualConfig {
#[serde(flatten)]
pub(crate) customer_facing: DittoConfig,
#[serde(flatten)]
pub(crate) internal: InternalConfig,
}
impl DittoConfig {
pub fn new<S: Into<String>>(database_id: S, connect: DittoConfigConnect) -> Self {
DittoConfig {
database_id: database_id.into(),
connect,
persistence_directory: None,
experimental: Default::default(),
system_parameters: Default::default(),
}
}
pub fn with_persistence_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.persistence_directory = Some(path.into());
self
}
pub(crate) fn validate_system_parameter_value_shapes(&self) -> Result<(), String> {
for (name, value) in &self.system_parameters {
let mut pending = vec![(value, 1_usize)];
while let Some((value, depth)) = pending.pop() {
if depth > SYSTEM_PARAMETER_DEPTH_SAFETY_CEILING
&& matches!(
value,
serde_json::Value::Array(_) | serde_json::Value::Object(_)
)
{
return Err(format!("system parameter `{name}` is nested too deeply"));
}
match value {
serde_json::Value::Null => {
return Err(format!(
"system parameter `{name}` contains JSON null; expected a boolean, \
string, number, array, or object"
));
}
serde_json::Value::Array(values) => {
pending.extend(values.iter().map(|value| (value, depth + 1)));
}
serde_json::Value::Object(values) => {
pending.extend(values.values().map(|value| (value, depth + 1)));
}
serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::String(_) => {}
}
}
}
Ok(())
}
}
#[serde_as]
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DittoConfigConnect {
Server {
url: Url,
},
SmallPeersOnly {
#[serde_as(as = "Option<Base64>")]
#[serde(skip_serializing_if = "Option::is_none")]
private_key: Option<Vec<u8>>,
},
}
#[doc(hidden)]
#[non_exhaustive]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DittoConfigExperimental {
#[serde(skip_serializing_if = "Option::is_none")]
pub passphrase: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_json_serialization_basic() {
let config = DittoConfig::new(
"test-database-id",
DittoConfigConnect::SmallPeersOnly { private_key: None },
);
let json = serde_json::to_string(&config).unwrap();
assert!(!json.contains("system_parameters"));
let deserialized: DittoConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.database_id, deserialized.database_id);
assert!(deserialized.system_parameters.is_empty());
assert!(deserialized.persistence_directory.is_none());
}
#[test]
fn test_config_json_with_system_parameters() {
let json = r#"{
"database_id": "test-database-id",
"connect": {
"type": "small_peers_only"
},
"system_parameters": {
"my_system_parameter": 42
}
}"#;
let deserialized: DittoConfig = serde_json::from_str(json).unwrap();
assert_eq!(deserialized.database_id, "test-database-id");
assert_eq!(
deserialized.system_parameters.get("my_system_parameter"),
Some(&serde_json::json!(42))
);
}
#[test]
fn test_config_cbor_from_default() {
let payload = ffi_sdk::dittoffi_ditto_config_default();
let _deserialized: DittoConfig = serde_cbor::from_slice(&payload).unwrap();
}
}