ratio-graph 0.23.3

Ratio's graph manipulation library.
Documentation
//! # Metadata module
//!
//! ## License
//!
//! This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
//! If a copy of the MPL was not distributed with this file,
//! You can obtain one at <https://mozilla.org/MPL/2.0/>.
//!
//! **Code examples both in the docstrings and rendered documentation are free to use.**

use std::collections::{BTreeMap, BTreeSet};

use uuid::Uuid;

#[cfg(feature = "serde")]
use crate::serde_utils::*;

/// Metadata struct that contains common metadata for any instance.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Metadata {
    /// Unique instance ID.
    #[cfg_attr(feature = "serde", serde(rename = "id"))]
    _id: Uuid,
    /// Instance name.
    pub name: String,
    /// Main category of this instance.
    pub kind: String,
    /// Instance labels.
    #[cfg_attr(
        feature = "serde",
        serde(
            default,
            serialize_with = "sort_alphabetically",
            skip_serializing_if = "is_empty_set"
        )
    )]
    pub labels: BTreeSet<String>,
    /// Numeric instance weights.
    #[cfg_attr(
        feature = "serde",
        serde(
            default,
            serialize_with = "sort_alphabetically",
            skip_serializing_if = "is_empty_map"
        )
    )]
    pub weights: BTreeMap<String, f64>,
    /// Miscellaneous data fields for this instance.
    #[cfg_attr(
        feature = "serde",
        serde(
            default,
            serialize_with = "sort_alphabetically",
            skip_serializing_if = "is_empty_map"
        )
    )]
    pub annotations: BTreeMap<String, serde_json::Value>,
}
impl Default for Metadata {
    fn default() -> Self {
        let _id = Uuid::new_v4();
        Self {
            _id,
            name: _id.to_string(),
            kind: "default".to_string(),
            labels: BTreeSet::<String>::default(),
            weights: BTreeMap::<String, f64>::default(),
            annotations: BTreeMap::<String, serde_json::Value>::default(),
        }
    }
}
impl Metadata {
    /// Create a new Meta struct with arguments.
    pub fn new(
        id: Option<Uuid>,
        name: Option<String>,
        kind: Option<String>,
        labels: Option<BTreeSet<String>>,
        weights: Option<BTreeMap<String, f64>>,
        annotations: Option<BTreeMap<String, serde_json::Value>>,
    ) -> Self {
        let _id = match id {
            None => Uuid::new_v4(),
            Some(x) => x,
        };
        Self {
            _id,
            name: name.unwrap_or_else(|| _id.to_string()),
            kind: kind.unwrap_or_else(|| "default".to_string()),
            labels: labels.unwrap_or_default(),
            weights: weights.unwrap_or_default(),
            annotations: annotations.unwrap_or_default(),
        }
    }
    /// The unique ID of this instance.
    pub fn id(&self) -> &Uuid {
        &self._id
    }
    /// Set the unique ID to a custom value (or generate a new one with None).
    /// Use with care! References in node and edge stores are NOT updated automatically.
    pub fn set_id(&mut self, value: Option<Uuid>) {
        self._id = value.unwrap_or_else(Uuid::new_v4);
    }
}

pub trait Meta {
    fn get_meta(&self) -> &Metadata;
    fn id(&self) -> &Uuid {
        &self.get_meta()._id
    }
    fn name(&self) -> &str {
        self.get_meta().name.as_str()
    }
    fn kind(&self) -> &str {
        self.get_meta().kind.as_str()
    }
    fn labels(&self) -> &BTreeSet<String> {
        &self.get_meta().labels
    }
    fn weights(&self) -> &BTreeMap<String, f64> {
        &self.get_meta().weights
    }
    fn annotations(&self) -> &BTreeMap<String, serde_json::Value> {
        &self.get_meta().annotations
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Aggregator {
    #[default]
    Binary,
    Kinds,
    Labels,
    Weights,
    Annotations,
}

#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize, serde::Deserialize),
    serde(default)
)]
pub struct Aggregate {
    /// Over how many items the aggregate has been taken.
    pub items: isize,
    /// Kind occurrence.
    pub kinds: BTreeMap<String, isize>,
    /// Label occurrence.
    pub labels: BTreeMap<String, isize>,
    /// Weight sum value.
    pub weights: BTreeMap<String, f64>,
    /// Annotation key occurrence.
    pub annotations: BTreeMap<String, isize>,
}
impl Aggregate {
    pub fn new(data: &[&Metadata]) -> Self {
        let mut agg = Self::default();
        for &d in data.iter() {
            agg.add(d)
        }
        agg
    }
    /// Add metadata to the aggregate.
    pub fn add(&mut self, item: &Metadata) {
        self.items += 1;
        *self.kinds.entry(item.kind.clone()).or_insert(0) += 1;
        for label in item.labels.iter() {
            *self.labels.entry(label.clone()).or_insert(0) += 1;
        }
        for (key, value) in item.weights.iter() {
            *self.weights.entry(key.clone()).or_insert(0.0) += value;
        }
        for key in item.annotations.keys() {
            *self.annotations.entry(key.clone()).or_insert(0) += 1;
        }
    }
    /// Subtract metadata from the aggregate.
    pub fn subtract(&mut self, item: &Metadata) {
        self.items -= 1;
        if let Some(&num) = self.kinds.get(&item.kind) {
            if num <= 1 {
                self.kinds.remove(&item.kind);
            } else {
                *self.kinds.entry(item.kind.clone()).or_insert(1) -= 1;
            }
        }
        for label in item.labels.iter() {
            if let Some(&num) = self.labels.get(label) {
                if num <= 1 {
                    self.labels.remove(label);
                } else {
                    *self.labels.entry(label.clone()).or_insert(1) -= 1;
                }
            }
        }
        for (key, value) in item.weights.iter() {
            *self.weights.entry(key.clone()).or_insert(0.0) -= value;
        }
        for key in item.annotations.keys() {
            if let Some(&num) = self.annotations.get(key) {
                if num <= 1 {
                    self.annotations.remove(key);
                } else {
                    *self.annotations.entry(key.clone()).or_insert(1) -= 1;
                }
            }
        }
    }
    /// Add another Aggregate's values to this.
    pub fn extend(&mut self, other: Self) -> &Self {
        self.items += other.items;
        for (key, value) in other.kinds {
            *self.kinds.entry(key).or_insert(0) += value;
        }
        for (key, value) in other.labels {
            *self.labels.entry(key).or_insert(0) += value;
        }
        for (key, value) in other.weights {
            *self.weights.entry(key).or_insert(0.0) += value;
        }
        for (key, value) in other.annotations {
            *self.annotations.entry(key).or_insert(0) += value;
        }
        self
    }
    /// Get the sum for all given fields' values for a given aggregator.
    pub fn sum(
        &self,
        aggregator: &Aggregator,
        fields: Option<&BTreeSet<String>>,
        absolute: bool,
    ) -> f64 {
        match aggregator {
            Aggregator::Binary => {
                if self.items > 0 {
                    1.0
                } else {
                    0.0
                }
            }
            Aggregator::Kinds => match fields {
                None => self.kinds.values().sum::<isize>() as f64,
                Some(fields) => fields
                    .iter()
                    .map(|field| self.value(aggregator, field, absolute))
                    .sum(),
            },
            Aggregator::Labels => match fields {
                None => self.labels.values().sum::<isize>() as f64,
                Some(fields) => fields
                    .iter()
                    .map(|field| self.value(aggregator, field, absolute))
                    .sum(),
            },
            Aggregator::Weights => match fields {
                None => self.weights.values().sum(),
                Some(fields) => fields
                    .iter()
                    .map(|field| self.value(aggregator, field, absolute))
                    .sum(),
            },
            Aggregator::Annotations => match fields {
                None => self.annotations.values().sum::<isize>() as f64,
                Some(fields) => fields
                    .iter()
                    .map(|field| self.value(aggregator, field, absolute))
                    .sum(),
            },
        }
    }
    /// Get the value that a field represents for a certain aggregator.
    pub fn value<S: AsRef<str>>(&self, aggregator: &Aggregator, field: &S, absolute: bool) -> f64 {
        let field = field.as_ref();
        let value = match aggregator {
            Aggregator::Binary => 1.0,
            Aggregator::Kinds => self.kinds.get(field).map(|&x| x as f64).unwrap_or(0.0),
            Aggregator::Labels => self.labels.get(field).map(|&x| x as f64).unwrap_or(0.0),
            Aggregator::Weights => self.weights.get(field).copied().unwrap_or(0.0),
            Aggregator::Annotations => self
                .annotations
                .get(field)
                .map(|&x| x as f64)
                .unwrap_or(0.0),
        };
        if absolute { value.abs() } else { value }
    }
    /// Get a fraction that a field represents for a certain aggregator.
    pub fn fraction<S: AsRef<str>>(
        &self,
        aggregator: &Aggregator,
        field: &S,
        fields: Option<&BTreeSet<String>>,
        absolute: bool,
    ) -> f64 {
        let sum = self.sum(aggregator, fields, absolute);
        let value = self.value(aggregator, field, absolute);
        if sum == 0.0 { 0.0 } else { value / sum }
    }
    /// Get all fractions for the given fields with respect to each other.
    pub fn fractions<S: AsRef<str>>(
        &self,
        aggregator: &Aggregator,
        fields: &[S],
        factor: f64,
        absolute: bool,
    ) -> Vec<f64> {
        let all: BTreeSet<String> = fields.iter().map(|s| s.as_ref().to_owned()).collect();
        let sum = self.sum(aggregator, Some(&all), absolute);
        let factor = { if sum == 0.0 { 1.0 } else { factor / sum } };
        fields
            .iter()
            .map(|field| factor * self.value(aggregator, field, absolute))
            .collect()
    }
}

/// Track lower and upper bounds for float BTreeMaps.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize, serde::Deserialize),
    serde(default)
)]
pub struct Domains {
    pub bounds: BTreeMap<String, (f64, f64)>,
}
impl Domains {
    /// Update the lower and upper bounds according to
    pub fn update(&mut self, values: &BTreeMap<String, f64>) {
        for (key, &value) in values.iter() {
            if let Some(entry) = self.bounds.get_mut(key) {
                entry.0 = entry.0.min(value);
                entry.1 = entry.1.max(value);
            } else {
                self.bounds.insert(key.to_owned(), (value, value));
            }
        }
    }
    /// Get the domains for a specified key.
    pub fn get<S: AsRef<str>>(&self, key: &S) -> Option<&(f64, f64)> {
        self.bounds.get(key.as_ref())
    }
    /// Interpolate a value for a specified key's domain. Returns 1.0 when the domain has no size
    /// (lower and upper bound are equal).
    pub fn interpolate<S: AsRef<str>>(&self, key: &S, value: f64) -> Option<f64> {
        self.get(key).map(|&(lower, upper)| {
            if lower == upper {
                1.0
            } else {
                (value - lower) / (upper - lower)
            }
        })
    }
}

/// Filtering options on metadata.
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize, serde::Deserialize),
    serde(default)
)]
pub struct MetadataFilter {
    pub kinds: Option<Vec<String>>,
    pub labels: Option<BTreeSet<String>>,
}
impl MetadataFilter {
    /// Whether some metadata holding object satisfies this filter.
    pub fn satisfies<T: Meta>(&self, instance: &T) -> bool {
        self.satisfies_kinds(instance) && self.satisfies_labels(instance)
    }

    /// Whether some metadata holding object satisfies the set kinds.
    pub fn satisfies_kinds<T: Meta>(&self, instance: &T) -> bool {
        if let Some(kinds) = &self.kinds {
            kinds.contains(&instance.kind().to_owned())
        } else {
            true
        }
    }
    /// Whether some metadata holding object satisfies the set labels.
    pub fn satisfies_labels<T: Meta>(&self, instance: &T) -> bool {
        {
            if let Some(labels) = &self.labels {
                !labels.is_disjoint(instance.labels())
            } else {
                true
            }
        }
    }
}