Skip to main content

hashtree_collection/
definition.rs

1use std::fmt;
2use std::sync::Arc;
3
4use hashtree_core::Cid;
5
6use crate::helpers::{normalize_search_entries, normalize_string_input, unique_strings};
7use crate::schema::CollectionSchema;
8use crate::{CollectionError, CollectionWriteContext};
9
10type CollectionIdFn<T> = Arc<dyn Fn(&T) -> String + Send + Sync>;
11type CollectionKeysFn<T> = Arc<dyn Fn(&T) -> Vec<String> + Send + Sync>;
12type CollectionSearchTextFn<T> = Arc<dyn Fn(&T) -> Vec<String> + Send + Sync>;
13type CollectionSearchEntriesFn<T> = Arc<
14    dyn for<'a> Fn(&T, &CollectionEntryContext<'a>) -> Vec<CollectionSearchEntry> + Send + Sync,
15>;
16
17pub fn default_search_prefix(name: &str) -> String {
18    format!("{name}:")
19}
20
21#[derive(Debug, Clone, PartialEq, Default)]
22pub struct CollectionPublishedSchema {
23    item_format: Option<String>,
24    projection_format: Option<String>,
25    schema_ref: Option<Cid>,
26}
27
28impl CollectionPublishedSchema {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn with_item_format(mut self, item_format: impl Into<String>) -> Self {
34        self.item_format = Some(item_format.into());
35        self
36    }
37
38    pub fn with_projection_format(mut self, projection_format: impl Into<String>) -> Self {
39        self.projection_format = Some(projection_format.into());
40        self
41    }
42
43    pub fn with_schema_ref(mut self, schema_ref: Cid) -> Self {
44        self.schema_ref = Some(schema_ref);
45        self
46    }
47
48    pub fn item_format(&self) -> Option<&str> {
49        self.item_format.as_deref()
50    }
51
52    pub fn projection_format(&self) -> Option<&str> {
53        self.projection_format.as_deref()
54    }
55
56    pub fn schema_ref(&self) -> Option<&Cid> {
57        self.schema_ref.as_ref()
58    }
59}
60
61#[derive(Clone)]
62pub struct CollectionKeyIndexDefinition<T> {
63    name: String,
64    keys: CollectionKeysFn<T>,
65}
66
67impl<T> CollectionKeyIndexDefinition<T> {
68    pub fn new(
69        name: impl Into<String>,
70        keys: impl Fn(&T) -> Vec<String> + Send + Sync + 'static,
71    ) -> Self {
72        Self {
73            name: name.into(),
74            keys: Arc::new(keys),
75        }
76    }
77
78    pub fn name(&self) -> &str {
79        &self.name
80    }
81
82    pub(crate) fn materialize_keys(&self, item: &T) -> Vec<String> {
83        unique_strings((self.keys)(item))
84    }
85}
86
87impl<T> fmt::Debug for CollectionKeyIndexDefinition<T> {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("CollectionKeyIndexDefinition")
90            .field("name", &self.name)
91            .finish()
92    }
93}
94
95#[derive(Debug, Clone)]
96pub struct CollectionEntryContext<'a> {
97    pub id: &'a str,
98    pub cid: Option<&'a Cid>,
99    pub write_context: Option<&'a CollectionWriteContext>,
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct CollectionSearchEntry {
104    pub text: Vec<String>,
105    pub id: Option<String>,
106    pub cid: Option<Cid>,
107    pub prefix: Option<String>,
108}
109
110impl CollectionSearchEntry {
111    pub fn new(text: Vec<String>) -> Self {
112        Self {
113            text,
114            id: None,
115            cid: None,
116            prefix: None,
117        }
118    }
119
120    pub fn with_id(mut self, id: impl Into<String>) -> Self {
121        self.id = Some(id.into());
122        self
123    }
124
125    pub fn with_cid(mut self, cid: Cid) -> Self {
126        self.cid = Some(cid);
127        self
128    }
129
130    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
131        self.prefix = Some(prefix.into());
132        self
133    }
134}
135
136#[derive(Clone)]
137pub struct CollectionSearchIndexDefinition<T> {
138    name: String,
139    root_name: Option<String>,
140    prefix: Option<String>,
141    options: hashtree_index::SearchIndexOptions,
142    text: Option<CollectionSearchTextFn<T>>,
143    entries: Option<CollectionSearchEntriesFn<T>>,
144}
145
146impl<T> CollectionSearchIndexDefinition<T> {
147    pub fn new(name: impl Into<String>) -> Self {
148        Self {
149            name: name.into(),
150            root_name: None,
151            prefix: None,
152            options: hashtree_index::SearchIndexOptions::default(),
153            text: None,
154            entries: None,
155        }
156    }
157
158    pub fn with_root_name(mut self, root_name: impl Into<String>) -> Self {
159        self.root_name = Some(root_name.into());
160        self
161    }
162
163    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
164        self.prefix = Some(prefix.into());
165        self
166    }
167
168    pub fn with_options(mut self, options: hashtree_index::SearchIndexOptions) -> Self {
169        self.options = options;
170        self
171    }
172
173    pub fn with_text(mut self, text: impl Fn(&T) -> Vec<String> + Send + Sync + 'static) -> Self {
174        self.text = Some(Arc::new(text));
175        self
176    }
177
178    pub fn with_entries(
179        mut self,
180        entries: impl for<'a> Fn(&T, &CollectionEntryContext<'a>) -> Vec<CollectionSearchEntry>
181            + Send
182            + Sync
183            + 'static,
184    ) -> Self {
185        self.entries = Some(Arc::new(entries));
186        self
187    }
188
189    pub fn name(&self) -> &str {
190        &self.name
191    }
192
193    pub fn root_name(&self) -> Option<&str> {
194        self.root_name.as_deref()
195    }
196
197    pub fn prefix(&self) -> Option<&str> {
198        self.prefix.as_deref()
199    }
200
201    pub fn options(&self) -> &hashtree_index::SearchIndexOptions {
202        &self.options
203    }
204
205    pub(crate) fn materialize_entries(
206        &self,
207        item: &T,
208        context: &CollectionEntryContext<'_>,
209    ) -> Vec<MaterializedCollectionSearchEntry> {
210        if let Some(entries) = self.entries.as_ref() {
211            return normalize_search_entries(entries(item, context));
212        }
213
214        let Some(text) = self
215            .text
216            .as_ref()
217            .map(|text| normalize_string_input(text(item)))
218            .filter(|text| !text.is_empty())
219        else {
220            return Vec::new();
221        };
222
223        vec![MaterializedCollectionSearchEntry {
224            text,
225            id: Some(context.id.to_string()),
226            cid: context.cid.cloned(),
227            prefix: self.prefix.clone(),
228        }]
229    }
230}
231
232impl<T> fmt::Debug for CollectionSearchIndexDefinition<T> {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("CollectionSearchIndexDefinition")
235            .field("name", &self.name)
236            .field("root_name", &self.root_name)
237            .field("prefix", &self.prefix)
238            .field("options", &self.options)
239            .finish()
240    }
241}
242
243#[derive(Debug, Clone, PartialEq)]
244pub(crate) struct MaterializedCollectionSearchEntry {
245    pub(crate) text: String,
246    pub(crate) id: Option<String>,
247    pub(crate) cid: Option<Cid>,
248    pub(crate) prefix: Option<String>,
249}
250
251#[derive(Clone)]
252pub struct CollectionDefinition<T> {
253    schema: Option<CollectionSchema<T>>,
254    published_schema: Option<CollectionPublishedSchema>,
255    get_id: CollectionIdFn<T>,
256    key_indexes: Vec<CollectionKeyIndexDefinition<T>>,
257    search_indexes: Vec<CollectionSearchIndexDefinition<T>>,
258}
259
260impl<T> CollectionDefinition<T> {
261    pub fn new(get_id: impl Fn(&T) -> String + Send + Sync + 'static) -> Self {
262        Self {
263            schema: None,
264            published_schema: None,
265            get_id: Arc::new(get_id),
266            key_indexes: Vec::new(),
267            search_indexes: Vec::new(),
268        }
269    }
270
271    pub fn with_schema(mut self, schema: CollectionSchema<T>) -> Self {
272        self.schema = Some(schema);
273        self
274    }
275
276    pub fn schema(&self) -> Option<&CollectionSchema<T>> {
277        self.schema.as_ref()
278    }
279
280    pub fn with_published_schema(mut self, published_schema: CollectionPublishedSchema) -> Self {
281        self.published_schema = Some(published_schema);
282        self
283    }
284
285    pub fn published_schema(&self) -> Option<&CollectionPublishedSchema> {
286        self.published_schema.as_ref()
287    }
288
289    pub fn with_key_index(
290        mut self,
291        name: impl Into<String>,
292        keys: impl Fn(&T) -> Vec<String> + Send + Sync + 'static,
293    ) -> Self {
294        self.key_indexes
295            .push(CollectionKeyIndexDefinition::new(name, keys));
296        self
297    }
298
299    pub fn with_search_index(mut self, index: CollectionSearchIndexDefinition<T>) -> Self {
300        self.search_indexes.push(index);
301        self
302    }
303
304    pub fn key_indexes(&self) -> &[CollectionKeyIndexDefinition<T>] {
305        &self.key_indexes
306    }
307
308    pub fn search_indexes(&self) -> &[CollectionSearchIndexDefinition<T>] {
309        &self.search_indexes
310    }
311
312    pub(crate) fn item_id(&self, item: &T) -> Result<String, CollectionError> {
313        let id = (self.get_id)(item).trim().to_string();
314        if id.is_empty() {
315            return Err(CollectionError::Validation(
316                "collection item id must not be empty".to_string(),
317            ));
318        }
319        Ok(id)
320    }
321}
322
323impl<T> fmt::Debug for CollectionDefinition<T> {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        f.debug_struct("CollectionDefinition")
326            .field(
327                "schema_version",
328                &self.schema.as_ref().map(|schema| schema.version()),
329            )
330            .field("published_schema", &self.published_schema)
331            .field("key_indexes", &self.key_indexes)
332            .field("search_indexes", &self.search_indexes)
333            .finish()
334    }
335}