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}