auditlog 0.1.0

Audit trail for your data models — an ORM-agnostic core with a pluggable, async sqlx backend (SQLite & Postgres).
Documentation
//! A simple in-memory [`Backend`], useful for unit tests and examples.

use std::sync::Mutex;

use async_trait::async_trait;

use crate::audit::{Audit, NewAudit};
use crate::backend::{AuditQuery, Backend, Order};
use crate::changes::AuditedChanges;
use crate::error::Result;
use crate::id::AuditId;

/// Stores audits in a `Vec` behind a `Mutex`. Not durable; intended for tests.
#[derive(Default)]
pub struct MemoryBackend {
    inner: Mutex<State>,
}

#[derive(Default)]
struct State {
    next_id: i64,
    audits: Vec<Audit>,
}

impl MemoryBackend {
    /// Create an empty backend.
    pub fn new() -> Self {
        MemoryBackend {
            inner: Mutex::new(State {
                next_id: 1,
                audits: Vec::new(),
            }),
        }
    }

    /// Total number of stored audits (across all auditables).
    pub fn total(&self) -> usize {
        self.inner.lock().unwrap().audits.len()
    }
}

fn sort(audits: &mut [Audit], order: Order) {
    match order {
        Order::VersionAsc => audits.sort_by(|a, b| a.version.cmp(&b.version).then(a.id.cmp(&b.id))),
        Order::VersionDesc => {
            audits.sort_by(|a, b| b.version.cmp(&a.version).then(b.id.cmp(&a.id)))
        }
        Order::CreatedAtAsc => {
            audits.sort_by(|a, b| a.created_at.cmp(&b.created_at).then(a.id.cmp(&b.id)))
        }
        Order::CreatedAtDesc => {
            audits.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id)))
        }
    }
}

fn apply_pagination(mut audits: Vec<Audit>, query: &AuditQuery) -> Vec<Audit> {
    if let Some(off) = query.offset {
        let off = off.max(0) as usize;
        audits = audits.into_iter().skip(off).collect();
    }
    if let Some(lim) = query.limit {
        let lim = lim.max(0) as usize;
        audits.truncate(lim);
    }
    audits
}

#[async_trait]
impl Backend for MemoryBackend {
    async fn insert(&self, audit: NewAudit) -> Result<Audit> {
        let mut state = self.inner.lock().unwrap();

        let version = if audit.action == crate::action::Action::Create {
            1
        } else {
            state
                .audits
                .iter()
                .filter(|a| {
                    a.auditable_type == audit.auditable_type && a.auditable_id == audit.auditable_id
                })
                .map(|a| a.version)
                .max()
                .unwrap_or(0)
                + 1
        };

        let id = state.next_id;
        state.next_id += 1;

        let persisted = Audit {
            id,
            auditable_type: audit.auditable_type,
            auditable_id: audit.auditable_id,
            associated_type: audit.associated_type,
            associated_id: audit.associated_id,
            user_type: audit.user_type,
            user_id: audit.user_id,
            username: audit.username,
            action: audit.action,
            audited_changes: audit.audited_changes,
            version,
            comment: audit.comment,
            remote_address: audit.remote_address,
            request_uuid: audit.request_uuid,
            created_at: audit.created_at,
        };
        state.audits.push(persisted.clone());
        Ok(persisted)
    }

    async fn audits_for_auditable(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
        query: &AuditQuery,
    ) -> Result<Vec<Audit>> {
        let state = self.inner.lock().unwrap();
        let mut out: Vec<Audit> = state
            .audits
            .iter()
            .filter(|a| a.auditable_type == auditable_type && &a.auditable_id == auditable_id)
            .filter(|a| query.matches(a))
            .cloned()
            .collect();
        sort(&mut out, query.order);
        Ok(apply_pagination(out, query))
    }

    async fn audits_for_associated(
        &self,
        associated_type: &str,
        associated_id: &AuditId,
        query: &AuditQuery,
    ) -> Result<Vec<Audit>> {
        let state = self.inner.lock().unwrap();
        let mut out: Vec<Audit> = state
            .audits
            .iter()
            .filter(|a| {
                a.associated_type.as_deref() == Some(associated_type)
                    && a.associated_id.as_ref() == Some(associated_id)
            })
            .filter(|a| query.matches(a))
            .cloned()
            .collect();
        sort(&mut out, query.order);
        Ok(apply_pagination(out, query))
    }

    async fn own_and_associated_audits(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
    ) -> Result<Vec<Audit>> {
        let state = self.inner.lock().unwrap();
        let mut out: Vec<Audit> = state
            .audits
            .iter()
            .filter(|a| {
                (a.auditable_type == auditable_type && &a.auditable_id == auditable_id)
                    || (a.associated_type.as_deref() == Some(auditable_type)
                        && a.associated_id.as_ref() == Some(auditable_id))
            })
            .cloned()
            .collect();
        sort(&mut out, Order::CreatedAtDesc);
        Ok(out)
    }

    async fn count_for_auditable(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
    ) -> Result<i64> {
        let state = self.inner.lock().unwrap();
        let n = state
            .audits
            .iter()
            .filter(|a| a.auditable_type == auditable_type && &a.auditable_id == auditable_id)
            .count();
        Ok(n as i64)
    }

    async fn find(&self, id: i64) -> Result<Option<Audit>> {
        let state = self.inner.lock().unwrap();
        Ok(state.audits.iter().find(|a| a.id == id).cloned())
    }

    async fn combine(
        &self,
        target_id: i64,
        merged_changes: &AuditedChanges,
        comment: Option<&str>,
        older_ids: &[i64],
    ) -> Result<()> {
        let mut state = self.inner.lock().unwrap();
        if let Some(target) = state.audits.iter_mut().find(|a| a.id == target_id) {
            target.audited_changes = merged_changes.clone();
            target.comment = comment.map(|c| c.to_string());
        }
        state.audits.retain(|a| !older_ids.contains(&a.id));
        Ok(())
    }
}