dittolive_ditto/ditto/init/config.rs
1use std::{collections::HashMap, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4use serde_with::{base64::Base64, serde_as};
5use url::Url;
6
7const SYSTEM_PARAMETER_DEPTH_SAFETY_CEILING: usize = 128;
8
9/// Configuration options required to initialize a [`Ditto`] instance.
10///
11/// # Example
12///
13/// ```
14/// # use dittolive_ditto::prelude::*;
15/// const YOUR_DATABASE_ID: &str = "REPLACE ME WITH YOUR APP/DATABASE ID";
16/// const YOUR_AUTH_URL: &str = "https://your-app.cloud.ditto.live";
17/// const YOUR_PRIVATE_KEY: &str = "REPLACE ME WITH YOUR PRIVATE KEY";
18///
19/// let connect = DittoConfigConnect::Server {
20/// url: YOUR_AUTH_URL.parse().unwrap(),
21/// };
22/// let connect = DittoConfigConnect::SmallPeersOnly {
23/// private_key: Some(YOUR_PRIVATE_KEY.as_bytes().to_vec()),
24/// };
25/// let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
26/// let config = DittoConfig::new(YOUR_DATABASE_ID, connect);
27/// ```
28///
29/// [`Ditto`]: crate::Ditto
30#[non_exhaustive]
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DittoConfig {
33 /// An ID used to distinguish between Ditto database instantiations.
34 ///
35 /// **Note**: This was formerly known as `appId`.
36 pub database_id: String,
37
38 /// The connection configuration this device uses to connect to a Ditto mesh.
39 pub connect: DittoConfigConnect,
40
41 /// The directory where Ditto will store its data.
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub persistence_directory: Option<PathBuf>,
44
45 #[doc(hidden)]
46 #[serde(default)]
47 pub experimental: DittoConfigExperimental,
48
49 /// System parameter overrides applied before Ditto initializes its subsystems.
50 ///
51 /// Each key is a system parameter name. Names are case-insensitive. Each value must match the
52 /// parameter's Core-defined type and validation rules. The supported [`serde_json::Value`]
53 /// shapes are:
54 ///
55 /// - booleans and strings;
56 /// - nonnegative integers through [`u64::MAX`], negative integers through [`i64::MIN`], and
57 /// finite floating-point numbers; and
58 /// - arrays and string-keyed objects containing those values recursively.
59 ///
60 /// Nonnegative integers are normalized to Core's unsigned integer representation, regardless
61 /// of whether they were constructed from a signed or unsigned Rust integer. JSON `null` is not
62 /// a system parameter type and is rejected, including inside arrays and objects. Binary values
63 /// and CBOR tags cannot be represented by [`serde_json::Value`].
64 ///
65 /// Unsupported SDK value shapes, such as JSON `null`, return
66 /// [`ErrorKind::InvalidInput`](crate::error::ErrorKind::InvalidInput). Unknown parameter
67 /// names, values of the wrong type, and values outside a parameter's valid range cause
68 /// [`Ditto::open`](crate::Ditto::open) and
69 /// [`Ditto::open_sync`](crate::Ditto::open_sync) to return
70 /// [`CoreApiErrorKind::ValidationInvalidDittoConfig`](crate::error::CoreApiErrorKind::ValidationInvalidDittoConfig).
71 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
72 pub system_parameters: HashMap<String, serde_json::Value>,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub(crate) struct InternalConfig {
77 /// SDK-computed v4 default persistence directory path for v4→v5 migration fallback.
78 ///
79 /// This is set automatically by `Ditto::open()` / `Ditto::open_sync()` when
80 /// `persistence_directory` is `None`. It should not normally be set by users.
81 ///
82 /// See: SDKS-3187
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub(crate) legacy_persistence_directory: Option<PathBuf>,
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88pub(crate) struct ActualConfig {
89 #[serde(flatten)]
90 pub(crate) customer_facing: DittoConfig,
91 #[serde(flatten)]
92 pub(crate) internal: InternalConfig,
93}
94
95impl DittoConfig {
96 /// Create a new [`DittoConfig`] instance from a database ID and connection configuration.
97 pub fn new<S: Into<String>>(database_id: S, connect: DittoConfigConnect) -> Self {
98 DittoConfig {
99 database_id: database_id.into(),
100 connect,
101 persistence_directory: None,
102 experimental: Default::default(),
103 system_parameters: Default::default(),
104 }
105 }
106
107 /// Optionally specify a custom persistence directory.
108 pub fn with_persistence_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
109 self.persistence_directory = Some(path.into());
110 self
111 }
112
113 pub(crate) fn validate_system_parameter_value_shapes(&self) -> Result<(), String> {
114 for (name, value) in &self.system_parameters {
115 let mut pending = vec![(value, 1_usize)];
116
117 while let Some((value, depth)) = pending.pop() {
118 if depth > SYSTEM_PARAMETER_DEPTH_SAFETY_CEILING
119 && matches!(
120 value,
121 serde_json::Value::Array(_) | serde_json::Value::Object(_)
122 )
123 {
124 return Err(format!("system parameter `{name}` is nested too deeply"));
125 }
126
127 match value {
128 serde_json::Value::Null => {
129 return Err(format!(
130 "system parameter `{name}` contains JSON null; expected a boolean, \
131 string, number, array, or object"
132 ));
133 }
134 serde_json::Value::Array(values) => {
135 pending.extend(values.iter().map(|value| (value, depth + 1)));
136 }
137 serde_json::Value::Object(values) => {
138 pending.extend(values.values().map(|value| (value, depth + 1)));
139 }
140 serde_json::Value::Bool(_)
141 | serde_json::Value::Number(_)
142 | serde_json::Value::String(_) => {}
143 }
144 }
145 }
146
147 Ok(())
148 }
149}
150
151/// Specifies the connection mechanism to be used by the [`Ditto`] instance.
152///
153/// # Example
154///
155/// ```
156/// use dittolive_ditto::prelude::*;
157/// let connect_config = DittoConfigConnect::Server {
158/// url: "https://example.com/your-server-url".parse().unwrap(),
159/// };
160/// let connect_config = DittoConfigConnect::SmallPeersOnly {
161/// private_key: Some("REPLACE_ME_WITH_YOUR_PRIVATE_KEY".bytes().collect()),
162/// };
163/// let connect_config = DittoConfigConnect::SmallPeersOnly { private_key: None };
164/// ```
165///
166/// [`Ditto`]: crate::Ditto
167#[serde_as]
168#[non_exhaustive]
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(tag = "type", rename_all = "snake_case")]
171pub enum DittoConfigConnect {
172 /// Connect to a Ditto "Big Peer" using the provided URL.
173 ///
174 /// When using `Server` mode, you _must_ provide an authentication expiration
175 /// handler using [`ditto.auth()?.set_expiration_handler(...)`][0].
176 ///
177 /// [0]: crate::prelude::DittoAuthenticator::set_expiration_handler
178 Server {
179 /// The "Auth URL" from the Ditto Portal, if using a Ditto-hosted Big Peer.
180 ///
181 /// If using a self-hosted Big Peer, provide your custom authentication URL.
182 ///
183 /// # Example
184 ///
185 /// ```text
186 /// // Where this subdomain is your Ditto `databaseId` (formerly `AppId`)
187 /// https://00000000-0000-0000-0000-000000000000.cloud.ditto.live
188 /// ```
189 url: Url,
190 },
191 /// Connect only to other Ditto small peers, using the provided private key.
192 ///
193 /// If no private key is provided, sync traffic will be UNENCRYPTED. This
194 /// mode should NOT be used in production, only for development and testing
195 /// purposes.
196 SmallPeersOnly {
197 /// Optional private key for encrypting sync traffic between small peers.
198 ///
199 /// If no private key is provided, sync traffic will be unencrypted. This
200 /// is meant only for development and testing use-cases. All production
201 /// use-cases should provide a private key so traffic will be encrypted.
202 #[serde_as(as = "Option<Base64>")]
203 #[serde(skip_serializing_if = "Option::is_none")]
204 private_key: Option<Vec<u8>>,
205 },
206}
207
208#[doc(hidden)]
209#[non_exhaustive]
210#[derive(Debug, Default, Clone, Serialize, Deserialize)]
211pub struct DittoConfigExperimental {
212 /// Retained for configuration compatibility. A non-null value is rejected when opening
213 /// Ditto.
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub passphrase: Option<String>,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_config_json_serialization_basic() {
224 let config = DittoConfig::new(
225 "test-database-id",
226 DittoConfigConnect::SmallPeersOnly { private_key: None },
227 );
228
229 let json = serde_json::to_string(&config).unwrap();
230 assert!(!json.contains("system_parameters"));
231 let deserialized: DittoConfig = serde_json::from_str(&json).unwrap();
232
233 assert_eq!(config.database_id, deserialized.database_id);
234 assert!(deserialized.system_parameters.is_empty());
235 assert!(deserialized.persistence_directory.is_none());
236 }
237
238 #[test]
239 fn test_config_json_with_system_parameters() {
240 let json = r#"{
241 "database_id": "test-database-id",
242 "connect": {
243 "type": "small_peers_only"
244 },
245 "system_parameters": {
246 "my_system_parameter": 42
247 }
248 }"#;
249 let deserialized: DittoConfig = serde_json::from_str(json).unwrap();
250
251 assert_eq!(deserialized.database_id, "test-database-id");
252 assert_eq!(
253 deserialized.system_parameters.get("my_system_parameter"),
254 Some(&serde_json::json!(42))
255 );
256 }
257
258 #[test]
259 fn test_config_cbor_from_default() {
260 let payload = ffi_sdk::dittoffi_ditto_config_default();
261 let _deserialized: DittoConfig = serde_cbor::from_slice(&payload).unwrap();
262 }
263}