dittolive-ditto 5.0.0

Ditto is a peer to peer cross-platform database that allows mobile, web, IoT and server apps to sync with or without an internet connection.
Documentation
use std::{collections::HashMap, path::PathBuf};

use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};
use url::Url;

/// Configuration options required to initialize a [`Ditto`] instance.
///
/// # Example
///
/// ```
/// # use dittolive_ditto::prelude::*;
/// const YOUR_DATABASE_ID: &str = "REPLACE ME WITH YOUR APP/DATABASE ID";
/// const YOUR_AUTH_URL: &str = "https://your-app.cloud.ditto.live";
/// const YOUR_PRIVATE_KEY: &str = "REPLACE ME WITH YOUR PRIVATE KEY";
///
/// let connect = DittoConfigConnect::Server {
///     url: YOUR_AUTH_URL.parse().unwrap(),
/// };
/// let connect = DittoConfigConnect::SmallPeersOnly {
///     private_key: Some(YOUR_PRIVATE_KEY.as_bytes().to_vec()),
/// };
/// let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
/// let config = DittoConfig::new(YOUR_DATABASE_ID, connect);
/// ```
///
/// [`Ditto`]: crate::Ditto
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DittoConfig {
    /// An ID used to distinguish between Ditto database instantiations.
    ///
    /// **Note**: This was formerly known as `appId`.
    pub database_id: String,

    /// The connection configuration this device uses to connect to a Ditto mesh.
    pub connect: DittoConfigConnect,

    /// The directory where Ditto will store its data.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub persistence_directory: Option<PathBuf>,

    #[doc(hidden)]
    #[serde(default)]
    pub experimental: DittoConfigExperimental,

    /// System Parameters that should be set on Ditto initialization.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub system_parameters: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct InternalConfig {
    /// SDK-computed v4 default persistence directory path for v4→v5 migration fallback.
    ///
    /// This is set automatically by `Ditto::open()` / `Ditto::open_sync()` when
    /// `persistence_directory` is `None`. It should not normally be set by users.
    ///
    /// See: SDKS-3187
    #[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 {
    /// Create a new [`DittoConfig`] instance from a database ID and connection configuration.
    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(),
        }
    }

    /// Optionally specify a custom persistence directory.
    pub fn with_persistence_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
        self.persistence_directory = Some(path.into());
        self
    }
}

/// Specifies the connection mechanism to be used by the [`Ditto`] instance.
///
/// # Example
///
/// ```
/// use dittolive_ditto::prelude::*;
/// let connect_config = DittoConfigConnect::Server {
///     url: "https://example.com/your-server-url".parse().unwrap(),
/// };
/// let connect_config = DittoConfigConnect::SmallPeersOnly {
///     private_key: Some("REPLACE_ME_WITH_YOUR_PRIVATE_KEY".bytes().collect()),
/// };
/// let connect_config = DittoConfigConnect::SmallPeersOnly { private_key: None };
/// ```
///
/// [`Ditto`]: crate::Ditto
#[serde_as]
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DittoConfigConnect {
    /// Connect to a Ditto "Big Peer" using the provided URL.
    ///
    /// When using `Server` mode, you _must_ provide an authentication expiration
    /// handler using [`ditto.auth()?.set_expiration_handler(...)`][0].
    ///
    /// [0]: crate::prelude::DittoAuthenticator::set_expiration_handler
    Server {
        /// The "Auth URL" from the Ditto Portal, if using a Ditto-hosted Big Peer.
        ///
        /// If using a self-hosted Big Peer, provide your custom authentication URL.
        ///
        /// # Example
        ///
        /// ```text
        /// // Where this subdomain is your Ditto `databaseId` (formerly `AppId`)
        /// https://00000000-0000-0000-0000-000000000000.cloud.ditto.live
        /// ```
        url: Url,
    },
    /// Connect only to other Ditto small peers, using the provided private key.
    ///
    /// If no private key is provided, sync traffic will be UNENCRYPTED. This
    /// mode should NOT be used in production, only for development and testing
    /// purposes.
    SmallPeersOnly {
        /// Optional private key for encrypting sync traffic between small peers.
        ///
        /// If no private key is provided, sync traffic will be unencrypted. This
        /// is meant only for development and testing use-cases. All production
        /// use-cases should provide a private key so traffic will be encrypted.
        #[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();
        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();
    }
}