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