use crate::face::{FaceError, FaceWatchStream, ResourceFormat, ResourceRef};
pub trait StoreBackend: Send + Sync + 'static {
fn name(&self) -> &str;
fn apply(&self, format: ResourceFormat, body: &[u8]) -> Result<(), FaceError>;
fn get(
&self,
reference: &ResourceRef,
format: ResourceFormat,
) -> Result<Vec<u8>, FaceError>;
fn list(
&self,
kind: &str,
namespace: Option<&str>,
format: ResourceFormat,
) -> Result<Vec<Vec<u8>>, FaceError>;
fn delete(&self, reference: &ResourceRef) -> Result<(), FaceError>;
fn watch(
&self,
kind: &str,
namespace: Option<&str>,
format: ResourceFormat,
) -> Result<Box<dyn FaceWatchStream>, FaceError>;
fn resource_count(&self) -> usize {
0
}
fn subscriber_count(&self) -> usize {
0
}
fn snapshot(&self) -> Result<Vec<u8>, FaceError> {
Err(FaceError::Unsupported(format!(
"snapshot not supported by {} backend",
self.name()
)))
}
fn restore(&self, _snapshot_bytes: &[u8]) -> Result<(), FaceError> {
Err(FaceError::Unsupported(format!(
"restore not supported by {} backend",
self.name()
)))
}
}
impl StoreBackend for crate::face_store::InMemoryStore {
fn name(&self) -> &str {
"in-memory"
}
fn apply(&self, format: ResourceFormat, body: &[u8]) -> Result<(), FaceError> {
self.apply(format, body)
}
fn get(
&self,
reference: &ResourceRef,
format: ResourceFormat,
) -> Result<Vec<u8>, FaceError> {
self.get(reference, format)
}
fn list(
&self,
kind: &str,
namespace: Option<&str>,
format: ResourceFormat,
) -> Result<Vec<Vec<u8>>, FaceError> {
self.list(kind, namespace, format)
}
fn delete(&self, reference: &ResourceRef) -> Result<(), FaceError> {
self.delete(reference)
}
fn watch(
&self,
kind: &str,
namespace: Option<&str>,
format: ResourceFormat,
) -> Result<Box<dyn FaceWatchStream>, FaceError> {
self.watch(kind, namespace, format)
}
fn resource_count(&self) -> usize {
self.len()
}
fn subscriber_count(&self) -> usize {
self.subscriber_count()
}
fn snapshot(&self) -> Result<Vec<u8>, FaceError> {
self.snapshot()
}
fn restore(&self, bytes: &[u8]) -> Result<(), FaceError> {
self.restore(bytes)
}
}
pub struct StubBackend {
backend_name: String,
}
impl StubBackend {
pub fn new(backend_name: impl Into<String>) -> Self {
Self {
backend_name: backend_name.into(),
}
}
}
impl StoreBackend for StubBackend {
fn name(&self) -> &str {
&self.backend_name
}
fn apply(&self, _format: ResourceFormat, _body: &[u8]) -> Result<(), FaceError> {
Err(FaceError::Unsupported(format!(
"apply: {} stub backend (real impl pending)",
self.backend_name
)))
}
fn get(
&self,
_reference: &ResourceRef,
_format: ResourceFormat,
) -> Result<Vec<u8>, FaceError> {
Err(FaceError::Unsupported(format!(
"get: {} stub backend (real impl pending)",
self.backend_name
)))
}
fn list(
&self,
_kind: &str,
_namespace: Option<&str>,
_format: ResourceFormat,
) -> Result<Vec<Vec<u8>>, FaceError> {
Err(FaceError::Unsupported(format!(
"list: {} stub backend (real impl pending)",
self.backend_name
)))
}
fn delete(&self, _reference: &ResourceRef) -> Result<(), FaceError> {
Err(FaceError::Unsupported(format!(
"delete: {} stub backend (real impl pending)",
self.backend_name
)))
}
fn watch(
&self,
_kind: &str,
_namespace: Option<&str>,
_format: ResourceFormat,
) -> Result<Box<dyn FaceWatchStream>, FaceError> {
Err(FaceError::Unsupported(format!(
"watch: {} stub backend (real impl pending)",
self.backend_name
)))
}
}
#[must_use]
pub fn raft_stub() -> Box<dyn StoreBackend> {
Box::new(StubBackend::new("openraft-coming-in-R5"))
}
#[must_use]
pub fn kube_apiserver_stub() -> Box<dyn StoreBackend> {
Box::new(StubBackend::new("kube-apiserver-coming-in-R6"))
}
#[must_use]
pub fn nomad_http_stub() -> Box<dyn StoreBackend> {
Box::new(StubBackend::new("nomad-http-coming-in-R6"))
}
#[must_use]
pub fn systemd_dbus_stub() -> Box<dyn StoreBackend> {
Box::new(StubBackend::new("systemd-dbus-coming-in-R6"))
}
#[must_use]
pub fn supervised_systemd_stub() -> Box<dyn StoreBackend> {
Box::new(StubBackend::new("supervised-systemd-coming-in-R6"))
}
#[cfg(test)]
mod tests {
use super::*;
fn pod_ref() -> ResourceRef {
ResourceRef::namespaced("Pod", "nginx", "default")
}
#[test]
fn store_backend_is_send_sync_static_dyn_compat() {
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
assert_send_sync_static::<Box<dyn StoreBackend>>();
}
#[test]
fn in_memory_store_impls_store_backend() {
let store = crate::face_store::InMemoryStore::new("test");
let backend: Box<dyn StoreBackend> = Box::new(store);
assert_eq!(backend.name(), "in-memory");
}
#[test]
fn stub_apply_errors_with_named_backend() {
let stub = StubBackend::new("raft-coming-soon");
match stub.apply(ResourceFormat::Native, b"") {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("raft-coming-soon"), "msg: {msg}");
assert!(msg.contains("apply"), "msg: {msg}");
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn stub_get_errors_with_named_backend() {
let stub = StubBackend::new("kube-tbd");
match stub.get(&pod_ref(), ResourceFormat::Yaml) {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("kube-tbd"));
assert!(msg.contains("get"));
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn stub_list_errors_with_named_backend() {
let stub = StubBackend::new("nomad-tbd");
match stub.list("Job", None, ResourceFormat::Hcl) {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("nomad-tbd"));
assert!(msg.contains("list"));
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn stub_delete_errors_with_named_backend() {
let stub = StubBackend::new("systemd-tbd");
match stub.delete(&pod_ref()) {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("systemd-tbd"));
assert!(msg.contains("delete"));
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn stub_watch_errors_with_named_backend() {
let stub = StubBackend::new("bms-tbd");
match stub.watch("Pod", None, ResourceFormat::Yaml) {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("bms-tbd"));
assert!(msg.contains("watch"));
}
Err(other) => panic!("expected Unsupported, got {other:?}"),
Ok(_) => panic!("expected Err, got Ok"),
}
}
#[test]
fn stub_snapshot_default_errors() {
let stub = StubBackend::new("any");
match stub.snapshot() {
Err(FaceError::Unsupported(msg)) => {
assert!(msg.contains("snapshot"), "msg: {msg}");
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn raft_stub_names_R5_milestone() {
let s = raft_stub();
assert!(s.name().contains("R5"), "name: {}", s.name());
assert!(s.name().contains("openraft"));
}
#[test]
fn kube_apiserver_stub_names_R6_milestone() {
let s = kube_apiserver_stub();
assert!(s.name().contains("R6"));
assert!(s.name().contains("kube-apiserver"));
}
#[test]
fn nomad_http_stub_names_R6_milestone() {
let s = nomad_http_stub();
assert!(s.name().contains("R6"));
assert!(s.name().contains("nomad"));
}
#[test]
fn systemd_dbus_stub_names_R6_milestone() {
let s = systemd_dbus_stub();
assert!(s.name().contains("R6"));
assert!(s.name().contains("systemd"));
}
#[test]
fn supervised_systemd_stub_names_R6_milestone() {
let s = supervised_systemd_stub();
assert!(s.name().contains("R6"));
assert!(s.name().contains("supervised"));
}
#[test]
fn heterogeneous_backend_vec_compiles_and_iterates() {
let backends: Vec<Box<dyn StoreBackend>> = vec![
Box::new(crate::face_store::InMemoryStore::new("mem")),
raft_stub(),
kube_apiserver_stub(),
nomad_http_stub(),
systemd_dbus_stub(),
supervised_systemd_stub(),
];
assert_eq!(backends.len(), 6);
assert_eq!(backends[0].name(), "in-memory");
assert!(backends[1..].iter().all(|b| {
b.name().contains("R5") || b.name().contains("R6")
}));
}
#[test]
fn in_memory_backend_via_trait_object_round_trips_apply_get() {
let backend: Box<dyn StoreBackend> = Box::new(
crate::face_store::InMemoryStore::new("test"),
);
let env = crate::face::encode_native_envelope(&pod_ref(), b"payload").unwrap();
backend.apply(ResourceFormat::Native, &env).unwrap();
let got = backend.get(&pod_ref(), ResourceFormat::Native).unwrap();
assert_eq!(got, env);
assert_eq!(backend.resource_count(), 1);
}
}