dittolive-ditto 4.0.0-beta1

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
#[macro_use]
extern crate serde_json;

mod common;

#[cfg(test)]
mod snippets {
    use std::{
        collections::HashMap,
        path::Path,
        sync::{mpsc::channel, Arc},
    };

    use dittolive_ditto::{
        error::DittoError,
        store::{
            ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
            ditto_attachment_token::DittoAttachmentToken,
        },
        types::DittoCounter,
    };
    use serde::Serialize;
    use uuid::Uuid;

    use super::common::*;

    #[derive(Serialize, Debug)]
    struct PersonId {
        user_id: String,
        work_id: i32,
    }

    #[test]
    fn basic() {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start datamodel
        let store = ditto.store();
        let collection = store.collection("people").unwrap();
        //@ditto/snippet-end

        //@ditto/snippet-start upsert-id
        let doc_id = DocumentId::new(&"123abc".to_string()).unwrap();
        let person = json!({ // Person implements serde::Serialize
            "_id": doc_id,
            "name": "Susan".to_string(),
            "age": 31,
        });
        collection.upsert(person).unwrap();
        //@ditto/snippet-end
    }

    #[test]
    fn array_to_map() {
        let ditto = get_ditto().unwrap();
        let store = ditto.store();
        let collection = store.collection("people").unwrap();
        let person = json!({
            "friends": ["Susan", "John"]
        });
        let doc_id = collection.upsert(person).unwrap();
        //@ditto/snippet-start array-to-map
        collection
            .find_by_id(doc_id)
            .update(|opt_doc| {
                if let Some(doc) = opt_doc {
                    let friends: DittoRegister = doc.get("friends").unwrap();
                    let mut map = HashMap::new();
                    let array = friends.value.as_array().unwrap();

                    for name in array {
                        let id = Uuid::new_v4().to_string();
                        let friend = json!({
                            "name": name,
                            "id": id
                        });
                        map.insert(id, friend);
                    }

                    doc.set("friendsMap", map).unwrap();
                }
            })
            .unwrap();
        //@ditto/snippet-end
    }

    #[test]
    fn upsert() {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start upsert
        let person = json!({
            "name": "Susan".to_string(),
            "age": 31,
        });
        let collection = ditto.store().collection("people").unwrap();
        let id = collection.upsert(person).unwrap();
        //@ditto/snippet-end

        //@ditto/snippet-start remove-id
        collection.find_by_id(id).remove().unwrap();
        //@ditto/snippet-end

        //@ditto/snippet-start upsert-composite-primary-key
        let collection = ditto.store().collection("people").unwrap();
        let complex_id = PersonId {
            user_id: "456abc".to_string(),
            work_id: 789,
        };
        let doc_id = DocumentId::new(&serde_json::json!(complex_id)).unwrap();
        let doc = json!({
            "_id": doc_id,
            "name": "Susan".to_string(),
            "age": 31,
        });
        collection.upsert(doc).unwrap();
        //@ditto/snippet-end

        //@ditto/snippet-start upsert-datatypes
        collection
            .upsert(json!({
              "boolean": true,
              "string": "Hello World",
              "number": 10,
              "map": {
                "key": "value"
              },
              "array": [1,2,3],
              "null": null,
            }))
            .unwrap();
        //@ditto/snippet-end

        //@ditto/snippet-start upsert-default-data
        let default_id = DocumentId::new(&"123abc".to_string()).unwrap();
        let data = json!({ // Person implements serde::Serialize
            "_id": default_id,
            "name": "Susan".to_string(),
            "age": 31,
        });
        collection
            .upsert_with_strategy(data, WriteStrategy::InsertDefaultIfAbsent)
            .unwrap();
        //@ditto/snippet-end
    }

    #[test]
    fn attachment() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        let images_dir = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
            .join("tests")
            .join("resources");
        //@ditto/snippet-start attachment
        let store = ditto.store();
        let collection = store.collection("foo")?;
        let attachment_file_path = images_dir.join("image.png");
        let mut metadata = HashMap::new();
        metadata.insert("some".to_owned(), "string".to_owned());
        let attachment =
            collection.new_attachment(attachment_file_path.to_str().unwrap(), metadata)?;
        let doc_id = DocumentId::new(&"123abc".to_string())?;
        let content = json!({"_id": doc_id, "some": "string", "my_attachment": attachment});
        let _ = collection.upsert(content)?;
        // Later or on another peer ...
        let doc = collection.find_by_id(doc_id).exec()?;
        let attachment_token = doc.get::<DittoAttachmentToken>("my_attachment")?;
        let (tx, rx) = channel();
        let m_tx = std::sync::Mutex::new(tx);
        let fetcher = collection.fetch_attachment(attachment_token, move |event| {
            // completion handler
            if let DittoAttachmentFetchEvent::Completed { attachment } = event {
                let tx = m_tx.lock().unwrap();
                tx.send(attachment).unwrap();
            }
        })?;
        let fetched_attachment = rx.recv().unwrap(); // may also use an async version or other sync strategy
        let attachment_file_path = fetched_attachment.path();
        std::fs::read(attachment_file_path)?;
        //@ditto/snippet-end
        drop(fetcher);
        Ok(())
    }

    #[test]
    fn counter() {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start counter
        let collection = ditto.store().collection("people").unwrap();
        let doc_id = collection
            .upsert(json!({"name": "Frank", "owned_cars": 0}))
            .unwrap();

        collection
            .find_by_id(doc_id)
            .update(|x| {
                if let Some(doc) = x {
                    doc.set("owned_cars", DittoCounter::new()).unwrap();
                    doc.increment("owned_cars", 1.0).unwrap();
                }
            })
            .unwrap();
        //@ditto/snippet-end
    }

    #[test]
    fn update() {
        let ditto = get_ditto().unwrap();

        //@ditto/snippet-start update
        let collection = ditto.store().collection("people").unwrap();
        let doc_id = collection
            .upsert(json!({"name": "Frank", "owned_cars": 0}))
            .unwrap();

        collection
            .find_by_id(doc_id)
            .update(|opt_doc| {
                if let Some(doc) = opt_doc {
                    doc.set("age", 32).unwrap();
                    doc.set("owned_cars", DittoCounter::new()).unwrap();
                    doc.increment("owned_cars", 1.0).unwrap();
                }
            })
            .unwrap();
        //@ditto/snippet-end
    }

    #[test]
    fn querying() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        let collection = ditto.store().collection("cars").unwrap();
        //@ditto/snippet-start query-basic
        collection
            .find("favoriteBook.title == \'The Great Gatsby\'")
            .exec()?;
        //@ditto/snippet-end

        //@ditto/snippet-start query-args
        let args = json!({"name": "Susan", "age": 32});
        collection
            .find_with_args("name == $args.name && age <= $args.age", args)
            .exec()?;
        //@ditto/snippet-end

        //@ditto/snippet-start query-sort
        let sort_param = ffi_sdk::COrderByParam {
            query_c_str: c!("miles"),
            direction: ffi_sdk::QuerySortDirection::Ascending,
        };
        collection
            .find("color == \'red\'")
            .sort(vec![sort_param])
            .exec()?;
        //@ditto/snippet-end

        //@ditto/snippet-start query-limit
        let sort_param = ffi_sdk::COrderByParam {
            query_c_str: c!("rank"),
            direction: ffi_sdk::QuerySortDirection::Ascending,
        };
        collection
            .find("color == \'red\'")
            .sort(vec![sort_param])
            .limit(100)
            .exec()?;
        //@ditto/snippet-end
        Ok(())
    }

    #[test]
    fn sync_subscribe() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start subscribe
        let store = ditto.store(); // Ditto must have a longer lifetime than all live queries
        let live_query = store
            .collection("cars")?
            .find("color == \'red\'")
            .subscribe();
        //@ditto/snippet-end
        drop(live_query);
        Ok(())
    }

    #[test]
    #[allow(deprecated)]
    fn sync_basic() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start sync-observe
        let store = ditto.store(); // Ditto must have a longer lifetime than all live queries
        let (tx, rx) = channel();
        {
            let live_query = store
                .collection("cars")?
                .find("color == \'red\'")
                .observe_local(move |mut docs: Vec<BoxedDocument>, event| {
                    match event {
                        LiveQueryEvent::Initial { .. } => { /* handle if appropriate */ }
                        LiveQueryEvent::Update { mut insertions, .. } => {
                            insertions.sort_by(|a, b| b.cmp(a));
                            for idx in insertions.iter() {
                                let doc = docs.remove(*idx);
                                tx.send(doc).unwrap();
                            }
                        }
                    }
                })?;
            store
                .collection("cars")?
                .upsert(json!({"color": "red"}))
                .unwrap();
            //@ditto/snippet-ignore-next-line
            #[allow(clippy::never_loop)]
            for doc in rx.iter() {
                println!("New doc {:?}", doc);
                //@ditto/snippet-ignore-next-line
                break;
            }
            //@ditto/snippet-ignore-next-line
            drop(live_query);
        } // IMPORTANT: LiveQuery goes out of scope and is Dropped and terminated here.
          //@ditto/snippet-end
        Ok(())
    }

    #[test]
    fn observe_local() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start sync-observe-local
        // Some action in your app ...
        let store = ditto.store();
        store.collection("cars")?.upsert(json!({"color": "red"}))?;
        // Elsewhere register handlers for data changes
        {
            let live_query = store
                .collection("cars")?
                .find("color == \'red\'")
                .observe_local(move |cars, event| {
                    println!("cars {:?}, event {:?}", cars, event);
                    // do something when data changes
                    // BUT this closure must be permitted to take ownership
                })?;
            // stash your live query in something with a long lifetime
            // or it will be dropped
            //@ditto/snippet-ignore-next-line
            drop(live_query);
        }
        //@ditto/snippet-end
        Ok(())
    }

    #[ignore]
    #[test]
    fn shared_key() -> Result<(), DittoError> {
        let license_token = std::env::var("DITTO_LICENSE").expect("No License Env Var provided");
        //@ditto/snippet-start shared-key
        // This is just an example. You should use OpenSSL to generate a unique shared key for every
        // application.
        let p256_der_b64: &str = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFUUrOkOH52QN+Rr6uDSDsk4hUTcD1eW4mT0UnGGptFehRANCAATJ3fG8TVLQcDwUV18BJJI8efK0hQAjzB3VJeYOVbfOlqnfukVId0V25r/abxwjD3HfHuPsCGEiefzzmkMbjPo9";
        let app_id = AppId::from_env("app")?;
        let ditto = Ditto::builder()
            .with_root(Arc::new(PersistentRoot::from_current_exe()?))
            .with_identity(|ditto_root| identity::SharedKey::new(ditto_root, app_id, p256_der_b64))?
            .with_minimum_log_level(CLogLevel::Info)
            .build()?;
        let res = ditto.set_offline_only_license_token(&license_token);
        ditto.start_sync()?;
        //@ditto/snippet-end
        assert!(res.is_ok());
        ditto.stop_sync();
        Ok(())
    }

    #[ignore]
    #[test]
    fn online_playground() -> Result<(), DittoError> {
        //@ditto/snippet-start online-playground
        let ditto = Ditto::builder()
            // creates a `ditto_data` folder in the directory containing the executing process
            .with_root(Arc::new(PersistentRoot::from_current_exe()?))
            .with_identity(|ditto_root| {
                // Provided as an env var, may also be provided as hardcoded string
                let app_id = AppId::from_env("00000000-0000-4000-0000-000000000000")?;
                let shared_token = std::env::var("REPLACE_ME_WITH_A_SHARED_TOKEN").unwrap();
                let enable_cloud_sync = true;
                let custom_auth_url = None;
                OnlinePlayground::new(
                    ditto_root,
                    app_id,
                    shared_token,
                    enable_cloud_sync,
                    custom_auth_url,
                )
            })?
            .build()?;

        ditto.start_sync()?;
        //@ditto/snippet-end
        ditto.stop_sync();
        Ok(())
    }

    #[ignore]
    #[test]
    fn offline_playground() -> Result<(), DittoError> {
        let license_token = std::env::var("DITTO_LICENSE").expect("No License Env Var provided");
        //@ditto/snippet-start offline-playground
        let ditto = Ditto::builder()
            // creates a `ditto_data` folder in the directory containing the executing process
            .with_root(Arc::new(PersistentRoot::from_current_exe()?))
            .with_identity(|ditto_root| {
                // Provided as an env var, may also be provided as hardcoded string
                let app_id = AppId::from_env("00000000-0000-4000-0000-000000000000")?;
                OfflinePlayground::new(ditto_root, app_id)
            })?
            .build()?;

        ditto.start_sync()?;
        let res = ditto.set_offline_only_license_token(&license_token);
        //@ditto/snippet-end
        assert!(res.is_ok());
        ditto.stop_sync();
        Ok(())
    }

    #[test]
    fn network_remote_ditto() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start network-remote-ditto
        let mut config = TransportConfig::new(); // empty

        config
            .connect
            .tcp_servers
            .insert("135.1.5.5:12345".to_string()); // Custom TCP Listener
        config
            .connect
            .tcp_servers
            .insert("185.1.5.5:12345".to_string()); // Custom TCP Listener
        config
            .connect
            .websocket_urls
            .insert("wss://example.com".to_string()); // Custom WS endpoint

        ditto.set_transport_config(config);
        ditto.start_sync()?;
        //@ditto/snippet-end
        ditto.stop_sync();
        Ok(())
    }

    #[test]
    fn network_listen() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start network-listen
        let mut config = TransportConfig::new(); // empty

        config.listen.tcp.enabled = true;
        config.listen.tcp.interface_ip = "0.0.0.0".to_string();
        config.listen.tcp.port = 4000;
        config.listen.http.enabled = false;

        ditto.set_transport_config(config);
        ditto.start_sync()?;
        //@ditto/snippet-end
        ditto.stop_sync();
        Ok(())
    }

    #[test]
    fn network_three() -> Result<(), DittoError> {
        let ditto = get_ditto().unwrap();
        //@ditto/snippet-start network-multiple-transports
        let mut config = TransportConfig::new(); // empty

        // 1. Enable auto-discovery of peer to peer connections
        config.enable_all_peer_to_peer(); // Auto-connect via lan and bluetooth

        // 2. Configure TCP Listener
        config.listen.tcp.enabled = true;
        config.listen.tcp.interface_ip = "0.0.0.0".to_string();
        config.listen.tcp.port = 4000;
        config.listen.http.enabled = false;

        // 3. Configure explicit, hard coded connections
        config
            .connect
            .tcp_servers
            .insert("135.1.5.5:12345".to_string()); // Custom TCP Listener
        config
            .connect
            .websocket_urls
            .insert("wss://example.com".to_string()); // Custom WS endpoint

        ditto.set_transport_config(config);
        ditto.start_sync()?;
        //@ditto/snippet-end
        ditto.stop_sync();
        Ok(())
    }

    // #[test]
    // fn query_overlap_test() -> Result<(), DittoError> {
    // let crew_a = get_ditto().unwrap();
    // let crew_b = get_ditto().unwrap();
    // let passenger = get_ditto().unwrap();
    //
    // passenger.store().collection("orders").find("user_id==abc123").observe_local(
    // move |mut docs: Vec<BoxedDocument>, event| {
    // render my orders
    // }
    // )
    //
    // crew_a.store().collection("orders").find_all().observe_local(
    // move |mut docs: Vec<BoxedDocument>, event| {
    // render all orders
    // }
    // )
    //
    // crew_b.store().collection("orders").find_all().observe_local(
    // move |mut docs: Vec<BoxedDocument>, event| {
    // render all orders
    // }
    // )
    //
    // Set up our query overlap group and priorities such that the crew members
    // will construct multihop connections with each other.
    //
    // crew_a.try_start_sync()?;
    // crew_b.try_start_sync()?;
    // passenger.try_start_sync()?;
    //
    // ditto.stop_sync();
    // Ok(())
    //
    // }
}