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