Skip to main content

ratio_metadata/
metadata.rs

1//! # Metadata module
2//!
3//! This module contains the standalone Metadata object as well as the MetadataTransaction builder.
4//! It defaults to settings a lot of the "field name" types to `String`, which is automatically
5//! inferred when you set the type of your new values before the `=` as well.
6//!
7//! ## License
8//!
9//! This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
10//! If a copy of the MPL was not distributed with this file,
11//! You can obtain one at <https://mozilla.org/MPL/2.0/>.
12//!
13//! **Code examples both in the docstrings and rendered documentation are free to use.**
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt::Debug;
17use std::ops::{AddAssign, SubAssign};
18
19use uuid::Uuid;
20
21use crate::transaction::{Transaction, TransactionMode};
22
23/// Field definition super-trait with blanket implementation. I.e. the name or reference to a field.
24/// May be used in itself to signal the presence of a categorical value, or may be the key to a
25/// weight or other sub-value.
26pub trait Field: Debug + Clone + Ord + PartialEq {}
27impl<T: Debug + Clone + Ord + PartialEq> Field for T {}
28
29/// Weight value super-trait with blanket implementation.
30pub trait WeightValue:
31    Debug
32    + Clone
33    + PartialEq
34    + PartialOrd
35    + AddAssign
36    + SubAssign
37    + num_traits::Signed
38    + num_traits::AsPrimitive<f64>
39{
40}
41impl<
42    T: Debug
43        + Clone
44        + PartialEq
45        + PartialOrd
46        + AddAssign
47        + SubAssign
48        + num_traits::Signed
49        + num_traits::AsPrimitive<f64>,
50> WeightValue for T
51{
52}
53
54/// Annotation value super-trait with blanket implementation.
55pub trait AnnotationValue: Debug + Clone + PartialEq {}
56impl<T: Debug + Clone + PartialEq> AnnotationValue for T {}
57
58/// String encoded metadata with float weights and free-form annotations.
59pub type SimpleMetadata = Metadata<String, String, String, String, f64, String, serde_json::Value>;
60
61/// Metadata for any object.
62#[derive(Clone, Debug, PartialEq, bon::Builder)]
63#[cfg_attr(
64    feature = "serde",
65    derive(serde::Serialize, serde::Deserialize),
66    serde(default, rename_all = "camelCase")
67)]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
70#[builder(on(String, into))]
71pub struct Metadata<N, K, L, W, WV, A, AV>
72where
73    N: Field,
74    K: Field,
75    L: Field,
76    W: Field,
77    WV: WeightValue,
78    A: Field,
79    AV: AnnotationValue,
80{
81    /// Instance identifier. Defaults to a randomly generated UUIDv4.
82    #[builder(default=Uuid::new_v4())]
83    pub id: Uuid,
84
85    /// Name associated with this instance.
86    #[builder(into)]
87    pub name: Option<N>,
88
89    /// Kind of object or main category associated with this instance.
90    #[builder(into)]
91    pub kind: Option<K>,
92
93    /// Set of labels associated with this instance.
94    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeSet::is_empty"))]
95    #[builder(default=BTreeSet::new())]
96    pub labels: BTreeSet<L>,
97
98    /// Numerical weights associated with this instance.
99    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeMap::is_empty"))]
100    #[builder(default=BTreeMap::new())]
101    pub weights: BTreeMap<W, WV>,
102
103    /// Free-form annotations associated with this instance.
104    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "BTreeMap::is_empty"))]
105    #[builder(default=BTreeMap::new())]
106    pub annotations: BTreeMap<A, AV>,
107}
108
109impl<N, K, L, W, WV, A, AV> Default for Metadata<N, K, L, W, WV, A, AV>
110where
111    N: Field,
112    K: Field,
113    L: Field,
114    W: Field,
115    WV: WeightValue,
116    A: Field,
117    AV: AnnotationValue,
118{
119    fn default() -> Self {
120        Self {
121            id: Uuid::new_v4(),
122            name: Default::default(),
123            kind: Default::default(),
124            labels: Default::default(),
125            weights: Default::default(),
126            annotations: Default::default(),
127        }
128    }
129}
130
131impl<'a, N, K, L, W, WV, A, AV> ReadMetadata<'a, N, K, L, W, WV, A, AV>
132    for Metadata<N, K, L, W, WV, A, AV>
133where
134    N: Field,
135    K: Field,
136    L: Field,
137    W: Field,
138    WV: WeightValue,
139    A: Field,
140    AV: AnnotationValue,
141{
142    fn id(&'a self) -> &'a Uuid {
143        &self.id
144    }
145
146    fn name(&'a self) -> Option<&'a N> {
147        self.name.as_ref()
148    }
149
150    fn kind(&'a self) -> Option<&'a K> {
151        self.kind.as_ref()
152    }
153
154    fn labels(&'a self) -> impl Iterator<Item = &'a L>
155    where
156        L: 'a,
157    {
158        self.labels.iter()
159    }
160
161    fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
162    where
163        W: 'a,
164        WV: 'a,
165    {
166        self.weights.iter()
167    }
168
169    fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
170    where
171        A: 'a,
172        AV: 'a,
173    {
174        self.annotations.iter()
175    }
176}
177
178impl<N, K, L, W, WV, A, AV> Metadata<N, K, L, W, WV, A, AV>
179where
180    N: Field,
181    K: Field,
182    L: Field,
183    W: Field,
184    WV: WeightValue,
185    A: Field,
186    AV: AnnotationValue,
187{
188    /// Creates a "replace" transaction from this metadata.
189    pub fn as_replace_transaction(self) -> Transaction<N, K, L, W, WV, A, AV> {
190        let Metadata {
191            id,
192            name,
193            kind,
194            labels,
195            weights,
196            annotations,
197        } = self;
198        Transaction::builder()
199            .mode(TransactionMode::Replace)
200            .id(id)
201            .maybe_name(name)
202            .maybe_kind(kind)
203            .labels(labels)
204            .weights(weights)
205            .annotations(annotations)
206            .build()
207    }
208
209    /// Creates an "append" transaction from this metadata.
210    pub fn as_append_transaction(self) -> Transaction<N, K, L, W, WV, A, AV> {
211        let Metadata {
212            id,
213            name,
214            kind,
215            labels,
216            weights,
217            annotations,
218        } = self;
219        Transaction::builder()
220            .mode(TransactionMode::Append)
221            .id(id)
222            .maybe_name(name)
223            .maybe_kind(kind)
224            .labels(labels)
225            .weights(weights)
226            .annotations(annotations)
227            .build()
228    }
229}
230
231impl<N, K, L, W, WV, A, AV> Metadata<N, K, L, W, WV, A, AV>
232where
233    N: Field,
234    K: Field,
235    L: Field,
236    W: Field,
237    WV: WeightValue,
238    A: Field,
239    AV: AnnotationValue,
240{
241    /// Create a metadata reference struct from this instance.
242    pub fn as_ref<'a>(&'a self) -> MetadataRef<'a, N, K, L, W, WV, A, AV> {
243        MetadataRef::builder()
244            .id(self.id)
245            .maybe_name(self.name.as_ref())
246            .maybe_kind(self.kind.as_ref())
247            .labels(self.labels.iter().collect())
248            .weights(self.weights.iter().collect())
249            .annotations(self.annotations.iter().collect())
250            .build()
251    }
252}
253
254/// Metadata for any object.
255#[derive(Clone, Debug, PartialEq, bon::Builder)]
256pub struct MetadataRef<'a, N, K, L, W, WV, A, AV>
257where
258    N: Field,
259    K: Field,
260    L: Field,
261    W: Field,
262    WV: WeightValue,
263    A: Field,
264    AV: AnnotationValue,
265{
266    /// Instance identifier. Defaults to a randomly generated UUIDv4.
267    pub id: Uuid,
268
269    /// Name associated with this instance.
270    pub name: Option<&'a N>,
271
272    /// Kind of object or main category associated with this instance.
273    pub kind: Option<&'a K>,
274
275    /// Set of labels associated with this instance.
276    #[builder(default=BTreeSet::new())]
277    pub labels: BTreeSet<&'a L>,
278
279    /// Numerical weights associated with this instance.
280    #[builder(default=BTreeMap::new())]
281    pub weights: BTreeMap<&'a W, &'a WV>,
282
283    /// Free-form annotations associated with this instance.
284    #[builder(default=BTreeMap::new())]
285    pub annotations: BTreeMap<&'a A, &'a AV>,
286}
287
288/// Simplified metadata reference where all fields are encoded as strings, weights as floats and
289/// free-form annotations.
290pub type SimpleMetadataRef<'a> =
291    MetadataRef<'a, String, String, String, String, f64, String, serde_json::Value>;
292
293impl<'a, N, K, L, W, WV, A, AV> MetadataRef<'a, N, K, L, W, WV, A, AV>
294where
295    N: Field,
296    K: Field,
297    L: Field,
298    W: Field,
299    WV: WeightValue,
300    A: Field,
301    AV: AnnotationValue,
302{
303    /// Clone this reference's contents into an owned metadata object.
304    pub fn cloned(&self) -> Metadata<N, K, L, W, WV, A, AV> {
305        Metadata {
306            id: self.id,
307            name: self.name.cloned(),
308            kind: self.kind.cloned(),
309            labels: self.labels.iter().map(|&label| label.to_owned()).collect(),
310            weights: self
311                .weights
312                .iter()
313                .map(|(&k, &v)| (k.to_owned(), v.to_owned()))
314                .collect(),
315            annotations: self
316                .annotations
317                .iter()
318                .map(|(&k, &v)| (k.to_owned(), v.to_owned()))
319                .collect(),
320        }
321    }
322}
323
324impl<'a, N, K, L, W, WV, A, AV> ReadMetadata<'a, N, K, L, W, WV, A, AV>
325    for MetadataRef<'a, N, K, L, W, WV, A, AV>
326where
327    N: Field,
328    K: Field,
329    L: Field,
330    W: Field,
331    WV: WeightValue,
332    A: Field,
333    AV: AnnotationValue,
334{
335    fn id(&'a self) -> &'a Uuid {
336        &self.id
337    }
338
339    fn name(&'a self) -> Option<&'a N> {
340        self.name
341    }
342
343    fn kind(&'a self) -> Option<&'a K> {
344        self.kind
345    }
346
347    fn labels(&'a self) -> impl Iterator<Item = &'a L>
348    where
349        L: 'a,
350    {
351        self.labels.iter().copied()
352    }
353
354    fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
355    where
356        W: 'a,
357        WV: 'a,
358    {
359        self.weights.clone().into_iter()
360    }
361
362    fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
363    where
364        A: 'a,
365        AV: 'a,
366    {
367        self.annotations.clone().into_iter()
368    }
369}
370
371/// Metadata read-only access trait.
372pub trait ReadMetadata<'a, N, K, L, W, WV, A, AV>
373where
374    N: Field,
375    K: Field,
376    L: Field,
377    W: Field,
378    WV: WeightValue,
379    A: Field,
380    AV: AnnotationValue,
381{
382    /// Get the ID from metadata.
383    fn id(&'a self) -> &'a Uuid;
384
385    /// Get the name from metadata.
386    fn name(&'a self) -> Option<&'a N>;
387
388    /// Get the kind from metadata.
389    fn kind(&'a self) -> Option<&'a K>;
390
391    /// Get the labels from metadata.
392    fn labels(&'a self) -> impl Iterator<Item = &'a L>
393    where
394        L: 'a;
395
396    /// Get the weights from metadata.
397    fn weights(&'a self) -> impl Iterator<Item = (&'a W, &'a WV)>
398    where
399        W: 'a,
400        WV: 'a;
401
402    /// Get the annotations from metadata.
403    fn annotations(&'a self) -> impl Iterator<Item = (&'a A, &'a AV)>
404    where
405        A: 'a,
406        AV: 'a;
407}
408
409#[cfg(test)]
410pub mod tests {
411    #[allow(unused_imports)]
412    use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
413    use serde_json::json;
414
415    #[allow(unused_imports)]
416    use super::*;
417
418    pub type SimpleTransaction =
419        Transaction<String, String, String, String, f64, String, serde_json::Value>;
420
421    #[derive(
422        Copy,
423        Clone,
424        Debug,
425        Default,
426        PartialEq,
427        PartialOrd,
428        Eq,
429        Ord,
430        strum::AsRefStr,
431        strum::Display,
432        strum::EnumIter,
433        strum::EnumString,
434        strum::FromRepr,
435        strum::IntoStaticStr,
436        strum::VariantNames,
437    )]
438    #[cfg_attr(
439        feature = "serde",
440        derive(serde::Serialize, serde::Deserialize),
441        serde(rename_all = "camelCase")
442    )]
443    pub enum N {
444        #[default]
445        Foo,
446        Bar,
447        Baz,
448        Quux,
449    }
450
451    #[derive(
452        Copy,
453        Clone,
454        Debug,
455        Default,
456        PartialEq,
457        PartialOrd,
458        Eq,
459        Ord,
460        strum::AsRefStr,
461        strum::Display,
462        strum::EnumIter,
463        strum::EnumString,
464        strum::FromRepr,
465        strum::IntoStaticStr,
466        strum::VariantNames,
467    )]
468    #[cfg_attr(
469        feature = "serde",
470        derive(serde::Serialize, serde::Deserialize),
471        serde(rename_all = "camelCase")
472    )]
473    pub enum K {
474        #[default]
475        A,
476        B,
477        C,
478    }
479
480    #[derive(
481        Copy,
482        Clone,
483        Debug,
484        Default,
485        PartialEq,
486        PartialOrd,
487        Eq,
488        Ord,
489        strum::AsRefStr,
490        strum::Display,
491        strum::EnumIter,
492        strum::EnumString,
493        strum::FromRepr,
494        strum::IntoStaticStr,
495        strum::VariantNames,
496    )]
497    #[cfg_attr(
498        feature = "serde",
499        derive(serde::Serialize, serde::Deserialize),
500        serde(rename_all = "camelCase")
501    )]
502    pub enum L {
503        #[default]
504        A,
505        B,
506        C,
507    }
508
509    #[derive(
510        Copy,
511        Clone,
512        Debug,
513        Default,
514        PartialEq,
515        PartialOrd,
516        Eq,
517        Ord,
518        strum::AsRefStr,
519        strum::Display,
520        strum::EnumIter,
521        strum::EnumString,
522        strum::FromRepr,
523        strum::IntoStaticStr,
524        strum::VariantNames,
525    )]
526    #[cfg_attr(
527        feature = "serde",
528        derive(serde::Serialize, serde::Deserialize),
529        serde(rename_all = "camelCase")
530    )]
531    pub enum W {
532        #[default]
533        A,
534        B,
535        C,
536    }
537
538    #[derive(
539        Copy,
540        Clone,
541        Debug,
542        Default,
543        PartialEq,
544        PartialOrd,
545        Eq,
546        Ord,
547        strum::AsRefStr,
548        strum::Display,
549        strum::EnumIter,
550        strum::EnumString,
551        strum::FromRepr,
552        strum::IntoStaticStr,
553        strum::VariantNames,
554    )]
555    #[cfg_attr(
556        feature = "serde",
557        derive(serde::Serialize, serde::Deserialize),
558        serde(rename_all = "camelCase")
559    )]
560    pub enum A {
561        #[default]
562        A,
563        B,
564        C,
565    }
566
567    /// Wrap these in a TestMeta type for ease of use in tests.
568    pub type TestMeta = Metadata<N, K, L, W, f64, A, serde_json::Value>;
569    pub type TestRef<'a> = MetadataRef<'a, N, K, L, W, f64, A, serde_json::Value>;
570    pub type TestTx = Transaction<N, K, L, W, f64, A, serde_json::Value>;
571    impl TestMeta {
572        pub fn foo() -> TestMeta {
573            Metadata::builder()
574                .name(N::Foo)
575                .kind(K::A)
576                .labels(bon::set![L::A, L::B])
577                .weights(bon::map![W::A: 1.0])
578                .annotations(bon::map![A::A: "FOO"])
579                .build()
580        }
581
582        pub fn bar() -> TestMeta {
583            Metadata::builder()
584                .name(N::Bar)
585                .kind(K::B)
586                .labels(bon::set![L::B, L::C])
587                .weights(bon::map! {W::B: 2.0})
588                .annotations(bon::map! {A::A: 3.0, A::B: "hello"})
589                .build()
590        }
591
592        pub fn baz() -> TestMeta {
593            Metadata::builder()
594                .name(N::Baz)
595                .kind(K::C)
596                .labels(bon::set![L::C])
597                .weights(bon::map! {W::B: 2.0})
598                .annotations(bon::map! {A::A: 3.0, A::B: "world"})
599                .build()
600        }
601
602        pub fn quux() -> TestMeta {
603            Metadata::builder()
604                .name(N::Quux) // no kind
605                .annotations(bon::map! {A::A: "has no kind, labels, or weights"})
606                .build()
607        }
608
609        pub fn foobarbazquux() -> Vec<TestMeta> {
610            vec![Self::foo(), Self::bar(), Self::baz(), Self::quux()]
611        }
612    }
613
614    #[test]
615    fn test_meta_helpers() {
616        let foo = TestMeta::foo();
617
618        assert_eq!(foo.kind, Some(K::A));
619        assert_eq!(foo.labels, BTreeSet::from_iter([L::A, L::B]));
620        assert_eq!(foo.weights, BTreeMap::from_iter([(W::A, 1.0)]));
621    }
622
623    #[test]
624    fn test_tx_helpers() {
625        let mut bar = TestMeta::bar().as_replace_transaction();
626
627        assert_eq!(bar.name, Some(N::Bar));
628        assert_eq!(bar.kind, Some(K::B));
629        assert_eq!(bar.labels, Some(BTreeSet::from_iter([L::B, L::C])));
630        assert_eq!(bar.weights, Some(BTreeMap::from_iter([(W::B, 2.0)])));
631        assert_eq!(
632            bar.annotations,
633            Some(BTreeMap::from_iter([
634                (
635                    A::A,
636                    serde_json::Value::Number(serde_json::Number::from_f64(3.0).unwrap())
637                ),
638                (A::B, serde_json::Value::String("hello".to_string()))
639            ]))
640        );
641
642        assert!(bar.id.is_some());
643        bar.strip_id();
644        assert!(bar.id.is_none());
645    }
646
647    #[test]
648    fn test_metadata_default() {
649        let metadata: SimpleMetadata = Default::default();
650        assert_eq!(metadata.name, None);
651        assert_eq!(metadata.kind, None);
652        assert!(metadata.labels.is_empty());
653        assert!(metadata.weights.is_empty());
654        assert!(metadata.annotations.is_empty());
655    }
656
657    #[test]
658    fn test_metadata_transaction_default() {
659        let transaction: SimpleTransaction = Default::default();
660        assert_eq!(transaction.id, None);
661        assert_eq!(transaction.name, None);
662        assert_eq!(transaction.kind, None);
663        assert_eq!(transaction.labels, None);
664        assert_eq!(transaction.weights, None);
665        assert_eq!(transaction.annotations, None);
666    }
667
668    #[test]
669    fn test_metadata_serialization() {
670        let metadata = Metadata {
671            id: Uuid::new_v4(),
672            name: Some("TestName".to_string()),
673            kind: Some("TestKind".to_string()),
674            labels: BTreeSet::new(),
675            weights: BTreeMap::new(),
676            annotations: BTreeMap::new(),
677        };
678
679        let serialized = serde_json::to_string(&metadata).unwrap();
680        let deserialized: SimpleMetadata = serde_json::from_str(&serialized).unwrap();
681
682        assert_eq!(metadata, deserialized);
683    }
684
685    #[test]
686    fn test_metadata_transaction_serialization() {
687        let transaction = Transaction {
688            name: Some("TestName".to_string()),
689            ..Default::default()
690        };
691
692        let serialized = serde_json::to_string(&transaction).unwrap();
693        let deserialized: SimpleTransaction = serde_json::from_str(&serialized).unwrap();
694
695        assert_eq!(transaction, deserialized);
696    }
697
698    #[test]
699    fn test_metadata_with_labels_and_weights() {
700        let mut labels = BTreeSet::new();
701        labels.insert("label1".to_string());
702        labels.insert("label2".to_string());
703
704        let mut weights = BTreeMap::new();
705        weights.insert("weight1".to_string(), 1.0);
706        weights.insert("weight2".to_string(), 2.0);
707
708        let metadata: SimpleMetadata = Metadata {
709            name: Some("TestName".to_string()),
710            kind: Some("TestKind".to_string()),
711            labels,
712            weights,
713            ..Default::default()
714        };
715
716        assert!(!metadata.id.is_nil());
717        assert_eq!(metadata.labels.len(), 2);
718        assert_eq!(metadata.weights.len(), 2);
719    }
720
721    #[test]
722    fn test_metadata_transaction_with_annotations() {
723        let mut annotations = BTreeMap::new();
724        annotations.insert("key1".to_string(), json!("value1"));
725        annotations.insert("key2".to_string(), json!("value2"));
726
727        let transaction = Transaction {
728            mode: Default::default(),
729            id: Some(Uuid::new_v4()),
730            name: Some("TestName".to_string()),
731            kind: Some("TestKind".to_string()),
732            labels: None::<BTreeSet<String>>,
733            weights: None::<BTreeMap<String, f64>>,
734            annotations: Some(annotations),
735        };
736
737        assert_eq!(transaction.annotations.as_ref().unwrap().len(), 2);
738    }
739
740    #[test]
741    fn test_metadata_builder() {
742        let metadata = SimpleMetadata::builder()
743            .name("TestName".to_string())
744            .kind("TestKind".to_string())
745            .build();
746
747        assert_eq!(metadata.name, Some("TestName".to_string()));
748        assert_eq!(metadata.kind, Some("TestKind".to_string()));
749        assert!(metadata.labels.is_empty());
750        assert!(metadata.weights.is_empty());
751        assert!(metadata.annotations.is_empty());
752    }
753
754    #[test]
755    fn test_metadata_transaction_builder() {
756        let transaction = SimpleTransaction::builder()
757            .id(Uuid::new_v4())
758            .name("TestName".to_string())
759            .kind("TestKind".to_string())
760            .build();
761
762        assert!(transaction.id.is_some());
763        assert_eq!(transaction.name, Some("TestName".to_string()));
764        assert_eq!(transaction.kind, Some("TestKind".to_string()));
765        assert!(transaction.labels.is_none());
766        assert!(transaction.weights.is_none());
767        assert!(transaction.annotations.is_none());
768    }
769}