use std::sync::Arc;
use bytes::Bytes;
use futures::future::BoxFuture;
use uuid::Uuid;
use crate::Result;
use crate::filter::Filter;
use crate::label::Label;
use crate::name::ResourceName;
use crate::object::{Association, Object};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Precondition {
#[default]
Any,
Version(u64),
}
impl Precondition {
pub fn check(&self, current_version: u64) -> Result<()> {
match self {
Precondition::Any => Ok(()),
Precondition::Version(expected) if *expected == current_version => Ok(()),
Precondition::Version(_) => Err(crate::Error::Conflict),
}
}
}
const NULL: serde_json::Value = serde_json::Value::Null;
pub(crate) fn props_or_null(properties: &Option<serde_json::Value>) -> &serde_json::Value {
properties.as_ref().unwrap_or(&NULL)
}
const CURSOR_VERSION: u8 = 1;
#[derive(serde::Serialize, serde::Deserialize)]
struct Cursor {
v: u8,
k: Uuid,
q: u64,
}
pub(crate) fn encode_cursor(last_id: Uuid, q: u64) -> String {
use base64::Engine as _;
let json = serde_json::to_vec(&Cursor {
v: CURSOR_VERSION,
k: last_id,
q,
})
.expect("Cursor serializes");
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json)
}
pub(crate) fn decode_cursor(page_token: Option<String>, q: u64) -> Result<Option<Uuid>> {
use base64::Engine as _;
let Some(token) = page_token else {
return Ok(None);
};
let invalid = || crate::Error::invalid_argument("invalid page token");
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(token.as_bytes())
.map_err(|_| invalid())?;
let cursor: Cursor = serde_json::from_slice(&bytes).map_err(|_| invalid())?;
if cursor.v != CURSOR_VERSION || cursor.q != q {
return Err(invalid());
}
Ok(Some(cursor.k))
}
pub(crate) fn object_fingerprint<L: Label>(
label: L,
namespace: Option<&ResourceName>,
filter: Option<&Filter>,
) -> u64 {
use std::hash::{Hash as _, Hasher as _};
let mut h = std::collections::hash_map::DefaultHasher::new();
label.hash(&mut h);
namespace.map(ToString::to_string).hash(&mut h);
filter
.map(|f| serde_json::to_string(f).expect("Filter serializes"))
.hash(&mut h);
h.finish()
}
pub(crate) fn edge_fingerprint<L: Label>(query: &EdgeQuery<'_, L>) -> u64 {
use std::hash::{Hash as _, Hasher as _};
let mut h = std::collections::hash_map::DefaultHasher::new();
match query.endpoint {
EdgeEndpoint::From(id) => (0u8, id).hash(&mut h),
EdgeEndpoint::Into(id) => (1u8, id).hash(&mut h),
}
query.label.hash(&mut h);
query
.target_label
.map(|l| l.as_str().to_string())
.hash(&mut h);
query.target_id.hash(&mut h);
query.since.hash(&mut h);
query.until.hash(&mut h);
query
.filter
.map(|f| serde_json::to_string(f).expect("Filter serializes"))
.hash(&mut h);
h.finish()
}
pub(crate) fn paginate_keyset<T>(
mut items: Vec<T>,
max_results: Option<usize>,
key: impl Fn(&T) -> Uuid,
q: u64,
) -> (Vec<T>, Option<String>) {
let limit = max_results.unwrap_or(usize::MAX);
let has_more = items.len() > limit;
if has_more {
items.truncate(limit);
}
let next = has_more
.then(|| items.last().map(|last| encode_cursor(key(last), q)))
.flatten();
(items, next)
}
pub(crate) fn v7_lower_bound(t: chrono::DateTime<chrono::Utc>) -> Uuid {
let millis = t.timestamp_millis().max(0) as u128;
Uuid::from_u128(millis << 80)
}
#[async_trait::async_trait]
pub trait ObjectStoreReader<L: Label>: Send + Sync + 'static {
async fn get(&self, id: &Uuid) -> Result<Object<L>>;
async fn get_by_name(&self, label: L, name: &ResourceName) -> Result<Object<L>>;
async fn list(
&self,
label: L,
namespace: Option<&ResourceName>,
max_results: Option<usize>,
page_token: Option<String>,
) -> Result<(Vec<Object<L>>, Option<String>)>;
async fn search(
&self,
label: L,
namespace: Option<&ResourceName>,
filter: &Filter,
max_results: Option<usize>,
page_token: Option<String>,
) -> Result<(Vec<Object<L>>, Option<String>)> {
let q = object_fingerprint(label, namespace, Some(filter));
let cursor = decode_cursor(page_token, q)?;
let (all, _) = self.list(label, namespace, None, None).await?;
let matched: Vec<Object<L>> = all
.into_iter()
.filter(|o| filter.matches(props_or_null(&o.properties)))
.filter(|o| cursor.is_none_or(|k| o.id > k))
.collect();
Ok(paginate_keyset(matched, max_results, |o| o.id, q))
}
async fn get_sensitive(&self, id: &Uuid) -> Result<Option<Bytes>> {
let _ = id;
Ok(None)
}
}
#[async_trait::async_trait]
pub trait ObjectStore<L: Label>: ObjectStoreReader<L> + Send + Sync + 'static {
async fn create(
&self,
label: L,
name: &ResourceName,
properties: Option<serde_json::Value>,
id: Option<Uuid>,
sensitive: Option<Bytes>,
) -> Result<Object<L>>;
async fn update(
&self,
id: &Uuid,
properties: Option<serde_json::Value>,
precondition: Precondition,
sensitive: Option<Bytes>,
) -> Result<Object<L>>;
async fn rename(
&self,
id: &Uuid,
new_name: &ResourceName,
precondition: Precondition,
) -> Result<Object<L>>;
async fn delete(&self, id: &Uuid) -> Result<()>;
async fn set_sensitive(&self, id: &Uuid, sensitive: Bytes) -> Result<()> {
let _ = (id, sensitive);
Ok(())
}
}
#[async_trait::async_trait]
pub trait SecretObjectReader<L: Label>: ObjectStoreReader<L> {
async fn get_with_secrets(&self, id: &Uuid) -> Result<Object<L>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeEndpoint {
From(Uuid),
Into(Uuid),
}
pub struct EdgeQuery<'a, L: Label> {
pub endpoint: EdgeEndpoint,
pub label: &'a str,
pub target_label: Option<L>,
pub target_id: Option<Uuid>,
pub filter: Option<&'a Filter>,
pub since: Option<chrono::DateTime<chrono::Utc>>,
pub until: Option<chrono::DateTime<chrono::Utc>>,
pub max_results: Option<usize>,
pub page_token: Option<String>,
}
impl<'a, L: Label> EdgeQuery<'a, L> {
pub fn from(from_id: Uuid, label: &'a str) -> Self {
Self::anchored(EdgeEndpoint::From(from_id), label)
}
pub fn into(to_id: Uuid, label: &'a str) -> Self {
Self::anchored(EdgeEndpoint::Into(to_id), label)
}
fn anchored(endpoint: EdgeEndpoint, label: &'a str) -> Self {
Self {
endpoint,
label,
target_label: None,
target_id: None,
filter: None,
since: None,
until: None,
max_results: None,
page_token: None,
}
}
#[must_use]
pub fn target_label(mut self, target_label: L) -> Self {
self.target_label = Some(target_label);
self
}
#[must_use]
pub fn target_id(mut self, target_id: Uuid) -> Self {
self.target_id = Some(target_id);
self
}
#[must_use]
pub fn filter(mut self, filter: &'a Filter) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn since(mut self, since: chrono::DateTime<chrono::Utc>) -> Self {
self.since = Some(since);
self
}
#[must_use]
pub fn until(mut self, until: chrono::DateTime<chrono::Utc>) -> Self {
self.until = Some(until);
self
}
#[must_use]
pub fn page(mut self, max_results: Option<usize>, page_token: Option<String>) -> Self {
self.max_results = max_results;
self.page_token = page_token;
self
}
}
#[async_trait::async_trait]
pub trait AssociationStoreReader<L: Label>: Send + Sync + 'static {
async fn query_edges(
&self,
query: EdgeQuery<'_, L>,
) -> Result<(Vec<Association<L>>, Option<String>)>;
async fn count_edges(
&self,
endpoint: EdgeEndpoint,
label: &str,
target_label: Option<L>,
) -> Result<u64> {
let mut total: u64 = 0;
let mut token = None;
loop {
let (batch, next) = self
.query_edges(EdgeQuery {
endpoint,
label,
target_label,
target_id: None,
filter: None,
since: None,
until: None,
max_results: None,
page_token: token,
})
.await?;
total += batch.len() as u64;
match next {
Some(t) => token = Some(t),
None => break,
}
}
Ok(total)
}
}
#[async_trait::async_trait]
pub trait AssociationStore<L: Label>: AssociationStoreReader<L> + Send + Sync + 'static {
async fn add(
&self,
from_id: Uuid,
to_id: Uuid,
label: &str,
properties: Option<serde_json::Value>,
) -> Result<()>;
async fn remove(&self, from_id: Uuid, to_id: Uuid, label: &str) -> Result<()>;
}
#[async_trait::async_trait]
impl<L: Label, T: ObjectStoreReader<L>> ObjectStoreReader<L> for Arc<T> {
async fn get(&self, id: &Uuid) -> Result<Object<L>> {
T::get(self, id).await
}
async fn get_by_name(&self, label: L, name: &ResourceName) -> Result<Object<L>> {
T::get_by_name(self, label, name).await
}
async fn list(
&self,
label: L,
namespace: Option<&ResourceName>,
max_results: Option<usize>,
page_token: Option<String>,
) -> Result<(Vec<Object<L>>, Option<String>)> {
T::list(self, label, namespace, max_results, page_token).await
}
async fn search(
&self,
label: L,
namespace: Option<&ResourceName>,
filter: &Filter,
max_results: Option<usize>,
page_token: Option<String>,
) -> Result<(Vec<Object<L>>, Option<String>)> {
T::search(self, label, namespace, filter, max_results, page_token).await
}
async fn get_sensitive(&self, id: &Uuid) -> Result<Option<Bytes>> {
T::get_sensitive(self, id).await
}
}
#[async_trait::async_trait]
impl<L: Label, T: SecretObjectReader<L>> SecretObjectReader<L> for Arc<T> {
async fn get_with_secrets(&self, id: &Uuid) -> Result<Object<L>> {
T::get_with_secrets(self, id).await
}
}
#[async_trait::async_trait]
impl<L: Label, T: ObjectStore<L>> ObjectStore<L> for Arc<T> {
async fn create(
&self,
label: L,
name: &ResourceName,
properties: Option<serde_json::Value>,
id: Option<Uuid>,
sensitive: Option<Bytes>,
) -> Result<Object<L>> {
T::create(self, label, name, properties, id, sensitive).await
}
async fn update(
&self,
id: &Uuid,
properties: Option<serde_json::Value>,
precondition: Precondition,
sensitive: Option<Bytes>,
) -> Result<Object<L>> {
T::update(self, id, properties, precondition, sensitive).await
}
async fn rename(
&self,
id: &Uuid,
new_name: &ResourceName,
precondition: Precondition,
) -> Result<Object<L>> {
T::rename(self, id, new_name, precondition).await
}
async fn delete(&self, id: &Uuid) -> Result<()> {
T::delete(self, id).await
}
async fn set_sensitive(&self, id: &Uuid, sensitive: Bytes) -> Result<()> {
T::set_sensitive(self, id, sensitive).await
}
}
#[async_trait::async_trait]
impl<L: Label, T: AssociationStoreReader<L>> AssociationStoreReader<L> for Arc<T> {
async fn query_edges(
&self,
query: EdgeQuery<'_, L>,
) -> Result<(Vec<Association<L>>, Option<String>)> {
T::query_edges(self, query).await
}
async fn count_edges(
&self,
endpoint: EdgeEndpoint,
label: &str,
target_label: Option<L>,
) -> Result<u64> {
T::count_edges(self, endpoint, label, target_label).await
}
}
#[async_trait::async_trait]
impl<L: Label, T: AssociationStore<L>> AssociationStore<L> for Arc<T> {
async fn add(
&self,
from_id: Uuid,
to_id: Uuid,
label: &str,
properties: Option<serde_json::Value>,
) -> Result<()> {
T::add(self, from_id, to_id, label, properties).await
}
async fn remove(&self, from_id: Uuid, to_id: Uuid, label: &str) -> Result<()> {
T::remove(self, from_id, to_id, label).await
}
}
#[async_trait::async_trait]
impl<L: Label, T: Transactional<L>> Transactional<L> for Arc<T> {
async fn transaction<'a, U>(
&'a self,
f: Box<dyn for<'t> FnOnce(&'t dyn StoreExec<L>) -> BoxFuture<'t, Result<U>> + Send + 'a>,
) -> Result<U>
where
U: Send + 'a,
{
T::transaction(self, f).await
}
async fn begin(&self) -> Result<Box<dyn StoreTx<L>>> {
T::begin(self).await
}
}
pub trait StoreExec<L: Label>: ObjectStore<L> + AssociationStore<L> {}
impl<L: Label, T: ObjectStore<L> + AssociationStore<L>> StoreExec<L> for T {}
#[async_trait::async_trait]
pub trait StoreTx<L: Label>: StoreExec<L> {
async fn commit(self: Box<Self>) -> Result<()>;
async fn rollback(self: Box<Self>) -> Result<()>;
}
#[async_trait::async_trait]
pub trait Transactional<L: Label>: Send + Sync + 'static {
async fn transaction<'a, T>(
&'a self,
f: Box<dyn for<'t> FnOnce(&'t dyn StoreExec<L>) -> BoxFuture<'t, Result<T>> + Send + 'a>,
) -> Result<T>
where
T: Send + 'a;
async fn begin(&self) -> Result<Box<dyn StoreTx<L>>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v7_lower_bound_brackets_real_ids() {
let t = chrono::Utc::now();
let lo = v7_lower_bound(t);
let next = v7_lower_bound(t + chrono::Duration::milliseconds(1));
assert!(lo < next, "consecutive millisecond bounds are ordered");
let id = Uuid::new_v7(uuid::Timestamp::from_unix(
uuid::NoContext,
t.timestamp() as u64,
t.timestamp_subsec_nanos(),
));
assert!(id >= lo, "a real v7 id is at or after its ms lower bound");
assert!(id < next, "and strictly before the next ms bound");
}
#[test]
fn v7_lower_bound_clamps_pre_epoch() {
let pre = chrono::DateTime::<chrono::Utc>::from_timestamp(-100, 0).unwrap();
assert_eq!(v7_lower_bound(pre), Uuid::from_u128(0));
}
#[test]
fn cursor_roundtrips() {
let id = Uuid::now_v7();
let token = encode_cursor(id, 42);
assert_eq!(decode_cursor(Some(token), 42).unwrap(), Some(id));
}
#[test]
fn cursor_none_is_start() {
assert_eq!(decode_cursor(None, 7).unwrap(), None);
}
#[test]
fn cursor_rejected_on_fingerprint_mismatch() {
let token = encode_cursor(Uuid::now_v7(), 1);
assert!(decode_cursor(Some(token), 2).is_err());
}
#[test]
fn cursor_rejects_legacy_offset_token() {
assert!(decode_cursor(Some("2".to_string()), 0).is_err());
assert!(decode_cursor(Some("garbage".to_string()), 0).is_err());
}
#[test]
fn paginate_keyset_trims_and_tokenizes() {
let ids: Vec<Uuid> = (0..3).map(|_| Uuid::now_v7()).collect();
let (page, next) = paginate_keyset(ids.clone(), Some(2), |id| *id, 9);
assert_eq!(page, ids[..2]);
assert_eq!(decode_cursor(next, 9).unwrap(), Some(ids[1]));
let (page, next) = paginate_keyset(ids.clone(), Some(3), |id| *id, 9);
assert_eq!(page, ids);
assert!(next.is_none());
}
}