use crate::config::Scope;
use faucet_core::FaucetError;
use mongodb::bson::{self, Bson};
use mongodb::change_stream::event::ResumeToken;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub fn state_key(scope: &Scope) -> String {
match scope {
Scope::Cluster => "mongodb-cdc:cluster".to_string(),
Scope::Database { database } => format!("mongodb-cdc:db:{database}"),
Scope::Collection {
database,
collection,
} => {
format!("mongodb-cdc:coll:{database}.{collection}")
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Bookmark {
pub resume_token: Value,
#[serde(default, skip_serializing_if = "is_false")]
pub invalidate: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
impl Bookmark {
pub fn from_token(token: &ResumeToken) -> Result<Self, FaucetError> {
let bson = bson::to_bson(token)
.map_err(|e| FaucetError::State(format!("mongodb-cdc resume token serialize: {e}")))?;
Ok(Self {
resume_token: bson.into_relaxed_extjson(),
invalidate: false,
})
}
pub fn from_value(v: Value) -> Result<Self, FaucetError> {
serde_json::from_value(v)
.map_err(|e| FaucetError::State(format!("mongodb-cdc bookmark parse: {e}")))
}
pub fn to_value(&self) -> Result<Value, FaucetError> {
serde_json::to_value(self)
.map_err(|e| FaucetError::State(format!("mongodb-cdc bookmark serialize: {e}")))
}
pub fn to_token(&self) -> Result<ResumeToken, FaucetError> {
let bson: Bson = bson::to_bson(&self.resume_token)
.map_err(|e| FaucetError::State(format!("mongodb-cdc resume token to bson: {e}")))?;
bson::from_bson(bson)
.map_err(|e| FaucetError::State(format!("mongodb-cdc resume token decode: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn state_key_cluster() {
assert_eq!(state_key(&Scope::Cluster), "mongodb-cdc:cluster");
}
#[test]
fn state_key_database() {
assert_eq!(
state_key(&Scope::Database {
database: "app".into()
}),
"mongodb-cdc:db:app"
);
}
#[test]
fn state_key_collection() {
assert_eq!(
state_key(&Scope::Collection {
database: "app".into(),
collection: "users".into()
}),
"mongodb-cdc:coll:app.users"
);
}
#[test]
fn bookmark_value_round_trip() {
let b = Bookmark {
resume_token: json!({ "_data": "8264AB" }),
..Default::default()
};
let v = b.to_value().unwrap();
let parsed = Bookmark::from_value(v).unwrap();
assert_eq!(parsed, b);
}
#[test]
fn bookmark_token_round_trip() {
let b = Bookmark {
resume_token: json!({ "_data": "8264AB00" }),
..Default::default()
};
let token = b.to_token().unwrap();
let b2 = Bookmark::from_token(&token).unwrap();
assert_eq!(b2.resume_token["_data"], json!("8264AB00"));
}
#[test]
fn from_value_rejects_garbage() {
assert!(Bookmark::from_value(json!({})).is_err());
assert!(Bookmark::from_value(json!("bare")).is_err());
}
#[test]
fn invalidate_flag_serde_is_backward_compatible() {
let legacy = Bookmark::from_value(json!({ "resume_token": { "_data": "AA" } })).unwrap();
assert!(!legacy.invalidate);
let normal = Bookmark {
resume_token: json!({ "_data": "AA" }),
invalidate: false,
};
let v = normal.to_value().unwrap();
assert!(v.get("invalidate").is_none(), "false flag is omitted: {v}");
let inv = Bookmark {
resume_token: json!({ "_data": "BB" }),
invalidate: true,
};
let back = Bookmark::from_value(inv.to_value().unwrap()).unwrap();
assert!(back.invalidate);
}
}