faucet_cli/catalog/
mod.rs1pub mod model;
21pub mod snapshot;
22pub mod spec;
23
24pub use spec::CatalogSpec;
25
26use crate::error::{CliError, CliResult};
27use crate::serve::config::HistoryBackendSpec;
28use crate::serve::history::catalog::ConfigSnapshot;
29use crate::serve::history::{self, RunHistory, catalog::CatalogUpdate};
30use std::sync::Arc;
31use std::time::Duration;
32
33pub const DEFAULT_SAMPLE_RECORDS: usize = 100;
36
37#[derive(Clone)]
39pub struct CatalogHandle {
40 pub store: Arc<dyn RunHistory>,
41 pub run_id: Option<String>,
45 pub sample_records: usize,
47}
48
49impl std::fmt::Debug for CatalogHandle {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("CatalogHandle")
52 .field("run_id", &self.run_id)
53 .field("sample_records", &self.sample_records)
54 .finish_non_exhaustive()
55 }
56}
57
58pub async fn connect_from_spec(spec: &CatalogSpec) -> CliResult<CatalogHandle> {
63 let backend = parse_url(&spec.url)?;
64 let store = history::connect(
65 &backend,
66 Duration::from_secs(3600),
69 Duration::from_secs(30),
70 &uuid::Uuid::now_v7().to_string(),
71 )
72 .await?;
73 Ok(CatalogHandle {
74 store,
75 run_id: None,
76 sample_records: spec.sample_records,
77 })
78}
79
80fn parse_url(url: &str) -> CliResult<HistoryBackendSpec> {
82 match url {
83 "memory" => Ok(HistoryBackendSpec::Memory),
84 u if u.starts_with("postgres://") || u.starts_with("postgresql://") => {
85 Ok(HistoryBackendSpec::Postgres(u.to_string()))
86 }
87 u if u.starts_with("sqlite:") => Ok(HistoryBackendSpec::Sqlite(u.to_string())),
88 other => Err(CliError::Config(format!(
89 "catalog.url '{other}' is not recognised — expected 'memory', 'sqlite:<path>', \
90 or a 'postgres://…' URL"
91 ))),
92 }
93}
94
95pub async fn record(handle: &CatalogHandle, update: &CatalogUpdate) {
98 if let Err(e) = handle.store.catalog_record(update).await {
99 tracing::warn!(
100 pipeline = %update.pipeline,
101 row = %update.row,
102 error = %e,
103 "catalog write failed — run unaffected"
104 );
105 }
106}
107
108pub async fn record_config_snapshot(handle: &CatalogHandle, snapshot: &ConfigSnapshot) {
111 if let Err(e) = handle.store.catalog_record_config_snapshot(snapshot).await {
112 tracing::warn!(
113 pipeline = %snapshot.pipeline,
114 error = %e,
115 "config-snapshot write failed — run unaffected"
116 );
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[tokio::test]
125 async fn connect_memory_and_reject_unknown_scheme() {
126 let handle = connect_from_spec(&CatalogSpec {
127 url: "memory".into(),
128 sample_records: 25,
129 })
130 .await
131 .unwrap();
132 assert_eq!(handle.sample_records, 25);
133 assert!(handle.run_id.is_none());
134
135 let err = connect_from_spec(&CatalogSpec {
136 url: "mysql://nope".into(),
137 sample_records: 100,
138 })
139 .await
140 .unwrap_err();
141 assert!(err.to_string().contains("catalog.url"), "{err}");
142 }
143
144 #[test]
145 fn parse_url_recognises_all_three_schemes() {
146 assert!(matches!(
147 parse_url("sqlite:./cat.db"),
148 Ok(HistoryBackendSpec::Sqlite(u)) if u == "sqlite:./cat.db"
149 ));
150 assert!(matches!(
151 parse_url("postgres://h/db"),
152 Ok(HistoryBackendSpec::Postgres(_))
153 ));
154 assert!(matches!(
155 parse_url("postgresql://h/db"),
156 Ok(HistoryBackendSpec::Postgres(_))
157 ));
158 assert!(matches!(
159 parse_url("memory"),
160 Ok(HistoryBackendSpec::Memory)
161 ));
162 assert!(parse_url("bogus").is_err());
163 }
164
165 #[tokio::test]
166 async fn handle_debug_never_prints_the_store() {
167 let handle = connect_from_spec(&CatalogSpec {
168 url: "memory".into(),
169 sample_records: 7,
170 })
171 .await
172 .unwrap();
173 let dbg = format!("{handle:?}");
174 assert!(dbg.contains("sample_records: 7"), "{dbg}");
175 assert!(dbg.contains(".."), "non-exhaustive marker expected: {dbg}");
176 }
177}