auditlog 0.1.0

Audit trail for your data models — an ORM-agnostic core with a pluggable, async sqlx backend (SQLite & Postgres).
Documentation
//! Configuration introspection helpers. Use these in tests to assert how a model is audited.

use std::collections::HashSet;

use crate::action::Action;
use crate::auditable::Auditable;
use crate::config::AuditOptions;

/// A snapshot of how a type is configured to be audited.
#[derive(Clone, Debug)]
pub struct AuditConfigInfo {
    /// The polymorphic type name.
    pub auditable_type: String,
    /// `only` whitelist, if set.
    pub only: Option<Vec<String>>,
    /// `except` blacklist, if set.
    pub except: Option<Vec<String>>,
    /// Audited actions.
    pub on: Vec<Action>,
    /// Whether a comment is required.
    pub comment_required: bool,
    /// The associated/parent type recorded on audits, if any.
    pub associated_with: Option<String>,
    /// Redacted columns.
    pub redacted: Vec<String>,
    /// Effective retention cap.
    pub max_audits: Option<usize>,
}

impl AuditConfigInfo {
    /// Whether `action` is audited.
    pub fn audits(&self, action: Action) -> bool {
        self.on.contains(&action)
    }

    /// Whether the `only` set exactly equals the given columns.
    pub fn only_is<I, S>(&self, columns: I) -> bool
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let want: HashSet<String> = columns.into_iter().map(Into::into).collect();
        match &self.only {
            Some(have) => have.iter().cloned().collect::<HashSet<_>>() == want,
            None => false,
        }
    }

    /// Whether the `except` set exactly equals the given columns.
    pub fn except_is<I, S>(&self, columns: I) -> bool
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let want: HashSet<String> = columns.into_iter().map(Into::into).collect();
        match &self.except {
            Some(have) => have.iter().cloned().collect::<HashSet<_>>() == want,
            None => false,
        }
    }

    /// The audited columns given the full set of columns, the primary key, and the optional STI
    /// inheritance column.
    pub fn audited_columns(
        &self,
        all_columns: &[String],
        primary_key: &str,
        inheritance_column: Option<&str>,
    ) -> Vec<String> {
        // Rebuild options just enough to reuse the column derivation.
        let opts = AuditOptions {
            only: self.only.clone(),
            except: self.except.clone(),
            ..AuditOptions::default()
        };
        let non_audited = opts.non_audited_columns(all_columns, primary_key, inheritance_column);
        all_columns
            .iter()
            .filter(|c| !non_audited.contains(*c))
            .cloned()
            .collect()
    }
}

/// Introspect a type's audit configuration.
///
/// ```
/// # use auditlog::{Auditable, AuditOptions, AuditId, ValueMap};
/// # struct User; impl Auditable for User {
/// #   fn auditable_type() -> &'static str { "User" }
/// #   fn auditable_id(&self) -> AuditId { 1.into() }
/// #   fn audited_attributes(&self) -> ValueMap { Default::default() }
/// #   fn audit_options() -> AuditOptions { AuditOptions::builder().only(["name"]).comment_required(true).build() }
/// # }
/// let info = auditlog::matchers::audit_config::<User>();
/// assert!(info.only_is(["name"]));
/// assert!(info.comment_required);
/// assert!(info.audits(auditlog::Action::Create));
/// ```
pub fn audit_config<T: Auditable>() -> AuditConfigInfo {
    let o = T::audit_options();
    AuditConfigInfo {
        auditable_type: T::auditable_type().to_string(),
        only: o.only.clone(),
        except: o.except.clone(),
        on: o.on.clone(),
        comment_required: o.comment_required,
        associated_with: o.associated_with.clone(),
        redacted: o.redacted.clone(),
        max_audits: o.effective_max_audits(),
    }
}