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}