use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::ops::{AddAssign, SubAssign};
use uuid::Uuid;
use crate::transaction::{Transaction, TransactionMode};
pub trait Field: Debug + Clone + Ord + PartialEq {}
impl<T: Debug + Clone + Ord + PartialEq> Field for T {}
pub trait WeightValue:
Debug
+ Clone
+ PartialEq
+ PartialOrd
+ AddAssign
+ SubAssign
+ num_traits::Signed
+ num_traits::AsPrimitive<f64>
{
}
impl<
T: Debug
+ Clone
+ PartialEq
+ PartialOrd
+ AddAssign
+ SubAssign
+ num_traits::Signed
+ num_traits::AsPrimitive<f64>,
> WeightValue for T
{
}
pub trait AnnotationValue: Debug + Clone + PartialEq {}
impl<T: Debug + Clone + PartialEq> AnnotationValue for T {}
pub type SimpleMetadata = Metadata<String, String, String, String, f64, String, serde_json::Value>;
#[derive(Clone, Debug, PartialEq, bon::Builder)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default, rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
#[builder(on(String, into))]
pub struct Metadata<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
#[builder(default=Uuid::new_v4())]
pub id: Uuid,
#[builder(into)]
pub name: Option<N>,
#[builder(into)]
pub kind: Option<K>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeSet::is_empty"))]
#[builder(default=BTreeSet::new())]
pub labels: BTreeSet<L>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeMap::is_empty"))]
#[builder(default=BTreeMap::new())]
pub weights: BTreeMap<W, WV>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeMap::is_empty"))]
#[builder(default=BTreeMap::new())]
pub annotations: BTreeMap<A, AV>,
}
impl<N, K, L, W, WV, A, AV> Default for Metadata<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn default() -> Self {
Self {
id: Uuid::new_v4(),
name: Default::default(),
kind: Default::default(),
labels: Default::default(),
weights: Default::default(),
annotations: Default::default(),
}
}
}
impl<'a, N, K, L, W, WV, A, AV> ReadMetadata<'a, N, K, L, W, WV, A, AV>
for Metadata<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn id(&'a self) -> &'a Uuid {
&self.id
}
fn name(&'a self) -> Option<&'a N> {
self.name.as_ref()
}
fn kind(&'a self) -> Option<&'a K> {
self.kind.as_ref()
}
fn labels(&'a self) -> impl Iterator<Item = &'a L>
where
L: 'a,
{
self.labels.iter()
}
fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
where
W: 'a,
WV: 'a,
{
self.weights.iter()
}
fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
where
A: 'a,
AV: 'a,
{
self.annotations.iter()
}
}
impl<N, K, L, W, WV, A, AV> Metadata<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
pub fn as_replace_transaction(self) -> Transaction<N, K, L, W, WV, A, AV> {
let Metadata {
id,
name,
kind,
labels,
weights,
annotations,
} = self;
Transaction::builder()
.mode(TransactionMode::Replace)
.id(id)
.maybe_name(name)
.maybe_kind(kind)
.labels(labels)
.weights(weights)
.annotations(annotations)
.build()
}
pub fn as_append_transaction(self) -> Transaction<N, K, L, W, WV, A, AV> {
let Metadata {
id,
name,
kind,
labels,
weights,
annotations,
} = self;
Transaction::builder()
.mode(TransactionMode::Append)
.id(id)
.maybe_name(name)
.maybe_kind(kind)
.labels(labels)
.weights(weights)
.annotations(annotations)
.build()
}
}
impl<N, K, L, W, WV, A, AV> Metadata<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
pub fn as_ref<'a>(&'a self) -> MetadataRef<'a, N, K, L, W, WV, A, AV> {
MetadataRef::builder()
.id(self.id)
.maybe_name(self.name.as_ref())
.maybe_kind(self.kind.as_ref())
.labels(self.labels.iter().collect())
.weights(self.weights.iter().collect())
.annotations(self.annotations.iter().collect())
.build()
}
}
#[derive(Clone, Debug, PartialEq, bon::Builder)]
pub struct MetadataRef<'a, N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
pub id: Uuid,
pub name: Option<&'a N>,
pub kind: Option<&'a K>,
#[builder(default=BTreeSet::new())]
pub labels: BTreeSet<&'a L>,
#[builder(default=BTreeMap::new())]
pub weights: BTreeMap<&'a W, &'a WV>,
#[builder(default=BTreeMap::new())]
pub annotations: BTreeMap<&'a A, &'a AV>,
}
pub type SimpleMetadataRef<'a> =
MetadataRef<'a, String, String, String, String, f64, String, serde_json::Value>;
impl<'a, N, K, L, W, WV, A, AV> MetadataRef<'a, N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
pub fn cloned(&self) -> Metadata<N, K, L, W, WV, A, AV> {
Metadata {
id: self.id,
name: self.name.cloned(),
kind: self.kind.cloned(),
labels: self.labels.iter().map(|&label| label.to_owned()).collect(),
weights: self
.weights
.iter()
.map(|(&k, &v)| (k.to_owned(), v.to_owned()))
.collect(),
annotations: self
.annotations
.iter()
.map(|(&k, &v)| (k.to_owned(), v.to_owned()))
.collect(),
}
}
}
impl<'a, N, K, L, W, WV, A, AV> ReadMetadata<'a, N, K, L, W, WV, A, AV>
for MetadataRef<'a, N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn id(&'a self) -> &'a Uuid {
&self.id
}
fn name(&'a self) -> Option<&'a N> {
self.name
}
fn kind(&'a self) -> Option<&'a K> {
self.kind
}
fn labels(&'a self) -> impl Iterator<Item = &'a L>
where
L: 'a,
{
self.labels.iter().copied()
}
fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
where
W: 'a,
WV: 'a,
{
self.weights.clone().into_iter()
}
fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
where
A: 'a,
AV: 'a,
{
self.annotations.clone().into_iter()
}
}
pub trait ReadMetadata<'a, N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn id(&'a self) -> &'a Uuid;
fn name(&'a self) -> Option<&'a N>;
fn kind(&'a self) -> Option<&'a K>;
fn labels(&'a self) -> impl Iterator<Item = &'a L>
where
L: 'a;
fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
where
W: 'a,
WV: 'a;
fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
where
A: 'a,
AV: 'a;
}
#[cfg(test)]
pub mod tests {
#[allow(unused_imports)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use serde_json::json;
#[allow(unused_imports)]
use super::*;
pub type SimpleTransaction =
Transaction<String, String, String, String, f64, String, serde_json::Value>;
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
strum::AsRefStr,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::FromRepr,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub enum N {
#[default]
Foo,
Bar,
Baz,
Quux,
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
strum::AsRefStr,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::FromRepr,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub enum K {
#[default]
A,
B,
C,
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
strum::AsRefStr,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::FromRepr,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub enum L {
#[default]
A,
B,
C,
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
strum::AsRefStr,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::FromRepr,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub enum W {
#[default]
A,
B,
C,
}
#[derive(
Copy,
Clone,
Debug,
Default,
PartialEq,
PartialOrd,
Eq,
Ord,
strum::AsRefStr,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::FromRepr,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
pub enum A {
#[default]
A,
B,
C,
}
pub type TestMeta = Metadata<N, K, L, W, f64, A, serde_json::Value>;
pub type TestRef<'a> = MetadataRef<'a, N, K, L, W, f64, A, serde_json::Value>;
pub type TestTx = Transaction<N, K, L, W, f64, A, serde_json::Value>;
impl TestMeta {
pub fn foo() -> TestMeta {
Metadata::builder()
.name(N::Foo)
.kind(K::A)
.labels(bon::set![L::A, L::B])
.weights(bon::map![W::A: 1.0])
.annotations(bon::map![A::A: "FOO"])
.build()
}
pub fn bar() -> TestMeta {
Metadata::builder()
.name(N::Bar)
.kind(K::B)
.labels(bon::set![L::B, L::C])
.weights(bon::map! {W::B: 2.0})
.annotations(bon::map! {A::A: 3.0, A::B: "hello"})
.build()
}
pub fn baz() -> TestMeta {
Metadata::builder()
.name(N::Baz)
.kind(K::C)
.labels(bon::set![L::C])
.weights(bon::map! {W::B: 2.0})
.annotations(bon::map! {A::A: 3.0, A::B: "world"})
.build()
}
pub fn quux() -> TestMeta {
Metadata::builder()
.name(N::Quux) .annotations(bon::map! {A::A: "has no kind, labels, or weights"})
.build()
}
pub fn foobarbazquux() -> Vec<TestMeta> {
vec![Self::foo(), Self::bar(), Self::baz(), Self::quux()]
}
}
#[test]
fn test_meta_helpers() {
let foo = TestMeta::foo();
assert_eq!(foo.kind, Some(K::A));
assert_eq!(foo.labels, BTreeSet::from_iter([L::A, L::B]));
assert_eq!(foo.weights, BTreeMap::from_iter([(W::A, 1.0)]));
}
#[test]
fn test_tx_helpers() {
let mut bar = TestMeta::bar().as_replace_transaction();
assert_eq!(bar.name, Some(N::Bar));
assert_eq!(bar.kind, Some(K::B));
assert_eq!(bar.labels, Some(BTreeSet::from_iter([L::B, L::C])));
assert_eq!(bar.weights, Some(BTreeMap::from_iter([(W::B, 2.0)])));
assert_eq!(
bar.annotations,
Some(BTreeMap::from_iter([
(
A::A,
serde_json::Value::Number(serde_json::Number::from_f64(3.0).unwrap())
),
(A::B, serde_json::Value::String("hello".to_string()))
]))
);
assert!(bar.id.is_some());
bar.strip_id();
assert!(bar.id.is_none());
}
#[test]
fn test_metadata_default() {
let metadata: SimpleMetadata = Default::default();
assert_eq!(metadata.name, None);
assert_eq!(metadata.kind, None);
assert!(metadata.labels.is_empty());
assert!(metadata.weights.is_empty());
assert!(metadata.annotations.is_empty());
}
#[test]
fn test_metadata_transaction_default() {
let transaction: SimpleTransaction = Default::default();
assert_eq!(transaction.id, None);
assert_eq!(transaction.name, None);
assert_eq!(transaction.kind, None);
assert_eq!(transaction.labels, None);
assert_eq!(transaction.weights, None);
assert_eq!(transaction.annotations, None);
}
#[test]
fn test_metadata_serialization() {
let metadata = Metadata {
id: Uuid::new_v4(),
name: Some("TestName".to_string()),
kind: Some("TestKind".to_string()),
labels: BTreeSet::new(),
weights: BTreeMap::new(),
annotations: BTreeMap::new(),
};
let serialized = serde_json::to_string(&metadata).unwrap();
let deserialized: SimpleMetadata = serde_json::from_str(&serialized).unwrap();
assert_eq!(metadata, deserialized);
}
#[test]
fn test_metadata_transaction_serialization() {
let transaction = Transaction {
name: Some("TestName".to_string()),
..Default::default()
};
let serialized = serde_json::to_string(&transaction).unwrap();
let deserialized: SimpleTransaction = serde_json::from_str(&serialized).unwrap();
assert_eq!(transaction, deserialized);
}
#[test]
fn test_metadata_with_labels_and_weights() {
let mut labels = BTreeSet::new();
labels.insert("label1".to_string());
labels.insert("label2".to_string());
let mut weights = BTreeMap::new();
weights.insert("weight1".to_string(), 1.0);
weights.insert("weight2".to_string(), 2.0);
let metadata: SimpleMetadata = Metadata {
name: Some("TestName".to_string()),
kind: Some("TestKind".to_string()),
labels,
weights,
..Default::default()
};
assert!(!metadata.id.is_nil());
assert_eq!(metadata.labels.len(), 2);
assert_eq!(metadata.weights.len(), 2);
}
#[test]
fn test_metadata_transaction_with_annotations() {
let mut annotations = BTreeMap::new();
annotations.insert("key1".to_string(), json!("value1"));
annotations.insert("key2".to_string(), json!("value2"));
let transaction = Transaction {
mode: Default::default(),
id: Some(Uuid::new_v4()),
name: Some("TestName".to_string()),
kind: Some("TestKind".to_string()),
labels: None::<BTreeSet<String>>,
weights: None::<BTreeMap<String, f64>>,
annotations: Some(annotations),
};
assert_eq!(transaction.annotations.as_ref().unwrap().len(), 2);
}
#[test]
fn test_metadata_builder() {
let metadata = SimpleMetadata::builder()
.name("TestName".to_string())
.kind("TestKind".to_string())
.build();
assert_eq!(metadata.name, Some("TestName".to_string()));
assert_eq!(metadata.kind, Some("TestKind".to_string()));
assert!(metadata.labels.is_empty());
assert!(metadata.weights.is_empty());
assert!(metadata.annotations.is_empty());
}
#[test]
fn test_metadata_transaction_builder() {
let transaction = SimpleTransaction::builder()
.id(Uuid::new_v4())
.name("TestName".to_string())
.kind("TestKind".to_string())
.build();
assert!(transaction.id.is_some());
assert_eq!(transaction.name, Some("TestName".to_string()));
assert_eq!(transaction.kind, Some("TestKind".to_string()));
assert!(transaction.labels.is_none());
assert!(transaction.weights.is_none());
assert!(transaction.annotations.is_none());
}
}