1use chrono::{DateTime, Utc};
4use std::fmt;
5
6pub type TagId = String;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Quality {
23 Good,
24 Uncertain,
25 Bad,
26}
27
28impl Quality {
29 pub fn is_trustworthy(self) -> bool {
35 matches!(self, Quality::Good)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq)]
60pub struct TagValue {
61 pub tag: TagId,
62 pub value: String,
63 pub quality: Quality,
64 pub timestamp: Option<DateTime<Utc>>,
65}
66
67#[derive(Debug, Clone, PartialEq)]
74pub enum TagWrite {
75 Float(f32),
76 Raw(String),
77}
78
79#[derive(Debug, Clone, PartialEq)]
88pub struct WriteOutcome {
89 pub success: bool,
90 pub error_message: Option<String>,
91}
92
93impl WriteOutcome {
94 pub fn success() -> WriteOutcome {
96 WriteOutcome {
97 success: true,
98 error_message: None,
99 }
100 }
101
102 pub fn failure(error_message: impl Into<String>) -> WriteOutcome {
104 WriteOutcome {
105 success: false,
106 error_message: Some(error_message.into()),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113pub enum NamespaceOrganization {
114 Unspecified,
115 Flat,
116 Hierarchical,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum BrowseSource {
122 Unspecified,
123 Da3,
124 Da2,
125 Flat,
126 Derived,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub enum BrowseNodeKind {
132 Unspecified,
133 Branch,
134 Item,
135 BranchAndItem,
136}
137
138impl BrowseNodeKind {
139 pub fn is_branch(self) -> bool {
140 matches!(self, Self::Branch | Self::BranchAndItem)
141 }
142
143 pub fn is_item(self) -> bool {
144 matches!(self, Self::Item | Self::BranchAndItem)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct DriverCapabilities {
151 pub application_version: String,
152 pub protocol_version: String,
153 pub max_page_size: u32,
154 pub supports_browse_sessions: bool,
155 pub supports_search: bool,
156 pub organization: NamespaceOrganization,
157 pub source: BrowseSource,
158 pub supports_indexed_search: bool,
159 pub indexed_search_protocol_version: String,
160 pub max_indexed_search_results: u32,
161 pub search_index_state: SearchIndexState,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct BrowseNode {
167 pub node_key: String,
169 pub display_name: String,
171 pub kind: BrowseNodeKind,
172 pub item_id: Option<String>,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct BrowsePage {
179 pub session_id: String,
180 pub nodes: Vec<BrowseNode>,
181 pub next_page_token: Option<String>,
182 pub complete: bool,
183 pub organization: NamespaceOrganization,
184 pub source: BrowseSource,
185 pub warning: Option<String>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct BrowsePageRequest {
192 pub session_id: Option<String>,
193 pub parent_node_key: Option<String>,
194 pub page_token: Option<String>,
195 pub page_size: u32,
196 pub refresh: bool,
197}
198
199impl BrowsePageRequest {
200 pub fn root(page_size: u32) -> Self {
202 Self {
203 session_id: None,
204 parent_node_key: None,
205 page_token: None,
206 page_size,
207 refresh: false,
208 }
209 }
210
211 pub fn children(
213 session_id: impl Into<String>,
214 parent_node_key: impl Into<String>,
215 page_size: u32,
216 ) -> Self {
217 Self {
218 session_id: Some(session_id.into()),
219 parent_node_key: Some(parent_node_key.into()),
220 page_token: None,
221 page_size,
222 refresh: false,
223 }
224 }
225
226 pub fn next(
228 session_id: impl Into<String>,
229 parent_node_key: Option<String>,
230 page_token: impl Into<String>,
231 page_size: u32,
232 ) -> Self {
233 Self {
234 session_id: Some(session_id.into()),
235 parent_node_key,
236 page_token: Some(page_token.into()),
237 page_size,
238 refresh: false,
239 }
240 }
241
242 pub fn with_refresh(mut self, refresh: bool) -> Self {
243 self.refresh = refresh;
244 self
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub enum SearchMatchMode {
251 Exact,
252 Prefix,
253 Contains,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
258pub enum SearchIndexState {
259 Unspecified,
260 NotIndexed,
261 Partial,
262 Ready,
263 Stale,
264 Refreshing,
265 Promoting,
266 Failed,
267 Deleting,
268}
269
270impl SearchIndexState {
271 pub const fn as_str(self) -> &'static str {
272 match self {
273 Self::Unspecified => "unspecified",
274 Self::NotIndexed => "not_indexed",
275 Self::Partial => "partial",
276 Self::Ready => "ready",
277 Self::Stale => "stale",
278 Self::Refreshing => "refreshing",
279 Self::Promoting => "promoting",
280 Self::Failed => "failed",
281 Self::Deleting => "deleting",
282 }
283 }
284}
285
286impl fmt::Display for SearchIndexState {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 f.write_str(self.as_str())
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
294pub enum SearchIndexControlAction {
295 Pause,
296 Resume,
297 Cancel,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct SearchIndexRequest {
304 pub query: String,
305 pub match_mode: SearchMatchMode,
306 pub max_results: u32,
307}
308
309impl SearchIndexRequest {
310 pub fn new(query: impl Into<String>, match_mode: SearchMatchMode, max_results: u32) -> Self {
311 Self {
312 query: query.into(),
313 match_mode,
314 max_results,
315 }
316 }
317}
318
319#[derive(Debug, Clone, PartialEq)]
321pub struct IndexedSearchProgress {
322 pub branches_visited: u64,
323 pub entries_seen: u64,
324 pub unique_items: u64,
325 pub active_time_ms: u64,
326 pub paused_time_ms: u64,
327 pub items_per_second: f64,
328 pub estimated_remaining_ms: Option<u64>,
329}
330
331#[derive(Debug, Clone, PartialEq)]
333pub struct SearchIndexStatus {
334 pub server: String,
335 pub state: SearchIndexState,
336 pub auto_refresh_enabled: bool,
337 pub active_generation: u64,
338 pub entry_count: u64,
339 pub unique_item_count: u64,
340 pub started_at: Option<String>,
341 pub completed_at: Option<String>,
342 pub last_error: Option<String>,
343 pub database_bytes: u64,
344 pub organization: NamespaceOrganization,
345 pub source: BrowseSource,
346 pub progress: Option<IndexedSearchProgress>,
347 pub scheduler: IndexSchedulerDiagnostics,
348}
349
350#[derive(Debug, Clone, Default, PartialEq, Eq)]
352pub struct IndexSchedulerDiagnostics {
353 pub next_refresh_at: Option<String>,
354 pub last_attempt_at: Option<String>,
355 pub last_success_at: Option<String>,
356 pub last_success_duration_ms: Option<u64>,
357 pub retry_after: Option<String>,
358 pub consecutive_failures: u32,
359 pub circuit_open: bool,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct IndexedSearchMatch {
365 pub item_id: String,
366 pub display_name: String,
367 pub kind: BrowseNodeKind,
368 pub breadcrumbs: Vec<String>,
369}
370
371#[derive(Debug, Clone, PartialEq)]
373pub struct SearchIndexResponse {
374 pub matches: Vec<IndexedSearchMatch>,
375 pub has_more: bool,
376 pub status: SearchIndexStatus,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct SearchRequest {
382 pub query: String,
383 pub match_mode: SearchMatchMode,
384 pub session_id: Option<String>,
385 pub scope_node_key: Option<String>,
386 pub max_results: u32,
387 pub include_branches: bool,
388 pub refresh: bool,
389}
390
391impl SearchRequest {
392 pub fn new(query: impl Into<String>, match_mode: SearchMatchMode, max_results: u32) -> Self {
393 Self {
394 query: query.into(),
395 match_mode,
396 session_id: None,
397 scope_node_key: None,
398 max_results,
399 include_branches: false,
400 refresh: false,
401 }
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct BrowseBreadcrumb {
408 pub node_key: String,
409 pub display_name: String,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct SearchMatch {
415 pub node: BrowseNode,
416 pub breadcrumbs: Vec<BrowseBreadcrumb>,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct SearchProgress {
422 pub visited_nodes: u32,
423 pub matches: u32,
424 pub partial: bool,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct SearchCompleted {
430 pub complete: bool,
431 pub cancelled: bool,
432 pub truncated: bool,
433 pub warning: Option<String>,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum SearchEvent {
439 Match(SearchMatch),
440 Progress(SearchProgress),
441 Completed(SearchCompleted),
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn search_index_state_strings_cover_all_wire_states() {
450 let states = [
451 (SearchIndexState::Unspecified, "unspecified"),
452 (SearchIndexState::NotIndexed, "not_indexed"),
453 (SearchIndexState::Partial, "partial"),
454 (SearchIndexState::Ready, "ready"),
455 (SearchIndexState::Stale, "stale"),
456 (SearchIndexState::Refreshing, "refreshing"),
457 (SearchIndexState::Promoting, "promoting"),
458 (SearchIndexState::Failed, "failed"),
459 (SearchIndexState::Deleting, "deleting"),
460 ];
461
462 for (state, expected) in states {
463 assert_eq!(state.as_str(), expected);
464 assert_eq!(state.to_string(), expected);
465 }
466 }
467
468 #[test]
469 fn browse_page_request_builders_preserve_navigation_and_refresh_state() {
470 assert_eq!(
471 BrowsePageRequest::root(25),
472 BrowsePageRequest {
473 session_id: None,
474 parent_node_key: None,
475 page_token: None,
476 page_size: 25,
477 refresh: false,
478 }
479 );
480 assert_eq!(
481 BrowsePageRequest::children("session", "parent", 10),
482 BrowsePageRequest {
483 session_id: Some("session".into()),
484 parent_node_key: Some("parent".into()),
485 page_token: None,
486 page_size: 10,
487 refresh: false,
488 }
489 );
490 assert_eq!(
491 BrowsePageRequest::next("session", Some("parent".into()), "token", 5)
492 .with_refresh(true),
493 BrowsePageRequest {
494 session_id: Some("session".into()),
495 parent_node_key: Some("parent".into()),
496 page_token: Some("token".into()),
497 page_size: 5,
498 refresh: true,
499 }
500 );
501 }
502
503 #[test]
504 fn only_good_quality_is_trustworthy() {
505 assert!(Quality::Good.is_trustworthy());
506 assert!(!Quality::Uncertain.is_trustworthy());
507 assert!(!Quality::Bad.is_trustworthy());
508 }
509
510 #[test]
511 fn write_outcome_success_has_no_error_message() {
512 let outcome = WriteOutcome::success();
513 assert!(outcome.success);
514 assert_eq!(outcome.error_message, None);
515 }
516
517 #[test]
518 fn write_outcome_failure_carries_message() {
519 let outcome = WriteOutcome::failure("tag is read-only");
520 assert!(!outcome.success);
521 assert_eq!(outcome.error_message.as_deref(), Some("tag is read-only"));
522 }
523}