1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
use_prelude!();

pub mod document;
pub mod document_id;
pub mod pending_cursor_operation;
pub mod pending_id_specific_operation;
pub mod type_traits;

use std::mem::MaybeUninit;

use ::ffi_sdk::{Attachment, AttachmentFileOperation};
pub use document::*;
use pending_cursor_operation::*;
use pending_id_specific_operation::*;

use super::{
    ditto_attachment::DittoAttachment, ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
    ditto_attachment_fetcher::DittoAttachmentFetcher, ditto_attachment_token::DittoAttachmentToken,
};
use crate::{
    ditto::WeakDittoHandleWrapper,
    error::{DittoError, ErrorKind},
};

#[derive(Clone, Debug)]
pub struct Collection {
    pub(super) ditto: WeakDittoHandleWrapper,
    pub(super) collection_name: char_p::Box,
}

impl Collection {
    pub(crate) fn new(ditto: WeakDittoHandleWrapper, collection_name: String) -> Self {
        let collection_name = char_p::new(collection_name.as_str());
        Self {
            ditto,
            collection_name,
        }
    }

    pub fn name(&self) -> &str {
        self.collection_name.as_ref().to_str()
    }

    /// Generates a DittoPendingCursorOperation that can be used to find all
    /// documents in the collection at a point in time or you can chain a call
    /// to observe_local or subscribe if you want to get updates about documents
    /// in the collection over time. It can also be used to update, remove, or
    /// evict documents.
    pub fn find_all(&self) -> PendingCursorOperation<'_> {
        self.find("true")
    }

    /// Generates a DittoPendingCursorOperation with the provided query that can
    /// be used to find the documents matching the query at a point in time or
    /// you can chain a call to observe_local or subscribe if you want to get
    /// updates about documents matching the query as they occur. It can also be
    /// used to update, remove, or evict documents.
    pub fn find(&self, query: &'_ str) -> PendingCursorOperation<'_> {
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            None,
        )
    }

    /// Generates a DittoPendingCursorOperation with the provided query and
    /// query arguments that can be used to find the documents matching the
    /// query at a point in time or you can chain a call to observe_local or
    /// subscribe if you want to get updates about documents matching the query
    /// as they occur. It can also be used to update, remove, or evict
    /// documents.
    ///
    /// This is the recommended function to use when performing queries on a
    /// collection if you have any dynamic data included in the query string. It
    /// allows you to provide a query string with placeholders, in the form of
    /// `$args.my_arg_name`, along with an accompanying dictionary of arguments,
    /// in the form of `{ "my_arg_name": "some value" }`, and the placeholders
    /// will be appropriately replaced by the matching provided arguments from
    /// the dictionary. This includes handling things like wrapping strings in
    /// quotation marks and arrays in square brackets, for example.
    pub fn find_with_args<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        query: &'_ str,
        query_args: C,
    ) -> PendingCursorOperation<'_> {
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            Some(serde_cbor::to_vec(query_args.borrow()).unwrap()),
        )
    }

    /// Generates a DittoPendingIDSpecificOperation with the provided document
    /// ID that can be used to find the document at a point in time or you can
    /// chain a call to observe_local or subscribe if you want to get updates
    /// about the document over time. It can also be used to update, remove, or
    /// evict the document.
    pub fn find_by_id(&self, doc_id: impl Into<DocumentId>) -> PendingIdSpecificOperation {
        PendingIdSpecificOperation {
            ditto: self.ditto.clone(),
            collection_name: self.collection_name.to_owned(),
            doc_id: doc_id.into(),
        }
    }
}

/// Convenience impl to avoid having to type `.clone()` everywhere.
impl From<&DocumentId> for DocumentId {
    fn from(doc: &DocumentId) -> Self {
        doc.clone()
    }
}

impl Collection {
    /// Inserts a new document into the collection and returns its ID. If the
    /// document already exists, the provided document content will be merged
    /// with the existing document's content.
    pub fn upsert<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            None,
            WriteStrategy::Merge,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its ID. If the
    /// document already exists, the behavior is determined by the provided
    /// `write_strategy`.
    pub fn upsert_with_strategy<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
        write_strategy: WriteStrategy,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            None,
            write_strategy,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its assigned ID.
    /// Use this method when the content has already been serialized externally
    /// into a CBOR-formatted byte array
    pub(crate) fn insert_cbor(
        &self,
        cbor: &'_ [u8],
        id: Option<&DocumentId>,
        write_strategy: WriteStrategy,
        write_txn: Option<&'_ mut ffi_sdk::CWriteTransaction>,
    ) -> Result<DocumentId, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        let id: Option<c_slice::Ref<'_, u8>> = id.map(|id| id.as_ref().into());
        let write_strategy_rs = write_strategy.as_write_strategy_rs();

        let id = unsafe {
            ffi_sdk::ditto_collection_insert_value(
                &*ditto,
                self.collection_name.as_ref(),
                cbor.into(),
                id,
                write_strategy_rs,
                write_txn,
            )
        }
        .ok_or(ErrorKind::InvalidInput)?;

        Ok(id.to::<Box<[u8]>>().into())
    }

    /// Creates a new attachment, which can then be inserted into a document.
    ///
    /// The file residing at the provided path will be copied into the
    /// Ditto’s store. The DittoAttachment object that is returned is what you
    /// can then use to insert an attachment into a document.
    ///
    /// You can provide metadata about the attachment, which will be replicated
    /// to other peers alongside the file attachment.
    ///
    /// Below is a snippet to show how you can use the new_attachment
    /// functionality to insert an attachment into a document.
    pub fn new_attachment<P: AsRef<Path>>(
        &self,
        path: P,
        metadata: HashMap<String, String>,
    ) -> Result<DittoAttachment, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        use ::safer_ffi::prelude::AsOut;
        let source_path = char_p::new(path.as_ref().to_str().unwrap());
        let file_operation = AttachmentFileOperation::Copy;
        let mut slot = MaybeUninit::<Attachment>::uninit();
        let out_attachment: Out<'_, Attachment> = slot.as_out();
        let status = unsafe {
            ffi_sdk::ditto_new_attachment_from_file(
                &ditto,
                source_path.as_ref(),
                file_operation,
                out_attachment,
            )
        };
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            let attachment = unsafe { slot.assume_init() }; // safe assuming above ffi call was successful
            let ret = DittoAttachment::new(
                attachment.id.into(),
                attachment.len,
                metadata,
                self.ditto.clone(),
                attachment.handle,
            );
            Ok(ret)
        }
    }

    /// Fetch the attachment corresponding to the provided attachment token.
    /// * `onchange` - A closure that will be called when the status of the request to fetch the
    ///   attachment has
    /// changed. If the attachment is already available then this will be called
    /// almost immediately with a completed status value.
    pub fn fetch_attachment<'a>(
        &self,
        attachment_token: DittoAttachmentToken,
        on_fetch_event: impl Fn(DittoAttachmentFetchEvent) + Send + Sync + 'a,
    ) -> Result<DittoAttachmentFetcher<'a>, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        DittoAttachmentFetcher::new(attachment_token, ditto, on_fetch_event)
    }
}