faucet_source_mssql_cdc/
state.rs1use std::collections::BTreeMap;
14
15use faucet_core::FaucetError;
16use serde_json::Value;
17
18use crate::lsn::Lsn;
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct Bookmarks(BTreeMap<String, Lsn>);
24
25impl Bookmarks {
26 pub fn new() -> Self {
28 Self(BTreeMap::new())
29 }
30
31 pub fn get(&self, capture_instance: &str) -> Option<Lsn> {
33 self.0.get(capture_instance).copied()
34 }
35
36 pub fn set(&mut self, capture_instance: impl Into<String>, lsn: Lsn) {
38 self.0.insert(capture_instance.into(), lsn);
39 }
40
41 pub fn from_value(v: Value) -> Result<Self, FaucetError> {
46 match v {
47 Value::Null => Ok(Self::new()),
48 Value::Object(map) => {
49 let mut out = BTreeMap::new();
50 for (ci, lsn_val) in map {
51 let hex = lsn_val.as_str().ok_or_else(|| {
52 FaucetError::State(format!(
53 "mssql-cdc bookmark: value for {ci:?} must be an LSN hex string"
54 ))
55 })?;
56 let lsn = Lsn::from_hex(hex)
57 .map_err(|e| FaucetError::State(format!("mssql-cdc bookmark: {e}")))?;
58 out.insert(ci, lsn);
59 }
60 Ok(Self(out))
61 }
62 other => Err(FaucetError::State(format!(
63 "mssql-cdc bookmark must be a JSON object of capture_instance -> LSN hex, got {other}"
64 ))),
65 }
66 }
67
68 pub fn to_value(&self) -> Result<Value, FaucetError> {
70 let mut map = serde_json::Map::with_capacity(self.0.len());
71 for (ci, lsn) in &self.0 {
72 map.insert(ci.clone(), Value::String(lsn.to_hex()));
73 }
74 Ok(Value::Object(map))
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use serde_json::json;
82
83 fn lsn(hex: &str) -> Lsn {
84 Lsn::from_hex(hex).unwrap()
85 }
86
87 #[test]
88 fn set_get_round_trip() {
89 let mut b = Bookmarks::new();
90 assert!(b.get("dbo_Orders").is_none());
91 b.set("dbo_Orders", lsn("00000000000000000005"));
92 assert_eq!(b.get("dbo_Orders"), Some(lsn("00000000000000000005")));
93 }
94
95 #[test]
96 fn value_round_trip() {
97 let mut b = Bookmarks::new();
98 b.set("dbo_Orders", lsn("0000002a000000550003"));
99 b.set("dbo_Items", lsn("0000002a000000560001"));
100 let v = b.to_value().unwrap();
101 assert_eq!(Bookmarks::from_value(v).unwrap(), b);
102 }
103
104 #[test]
105 fn value_shape_is_ci_to_hex() {
106 let mut b = Bookmarks::new();
107 b.set("dbo_Orders", lsn("00000000000000000009"));
108 assert_eq!(
109 b.to_value().unwrap(),
110 json!({ "dbo_Orders": "00000000000000000009" })
111 );
112 }
113
114 #[test]
115 fn null_is_empty_map() {
116 assert_eq!(
117 Bookmarks::from_value(Value::Null).unwrap(),
118 Bookmarks::new()
119 );
120 }
121
122 #[test]
123 fn rejects_non_object() {
124 assert!(Bookmarks::from_value(json!("nope")).is_err());
125 assert!(Bookmarks::from_value(json!([1, 2, 3])).is_err());
126 }
127
128 #[test]
129 fn rejects_bad_lsn_value() {
130 assert!(Bookmarks::from_value(json!({ "dbo_Orders": 42 })).is_err());
131 assert!(Bookmarks::from_value(json!({ "dbo_Orders": "xx" })).is_err());
132 }
133}