vantage-vista 2.5.0

Universal, schema-bearing data handle for the Vantage data framework
Documentation
//! The immutable table handle scripts narrow: a base plus a list of steps.
//!
//! Every narrowing verb returns a new [`Handle`] that shares its prefix with
//! the one it came from, so a handle stored in a variable never changes. A
//! [`Vista`] is only built when a terminal asks for one, through
//! [`Handle::resolve`].
//!
//! In a `modify:` script, `self` is a handle over the Vista being
//! changed, and it follows the same rule: `self.where(..)` returns a new
//! handle, and a statement whose result is dropped changes nothing. The
//! script's value is the handle it ends on.

mod query;
mod step;

use std::sync::Arc;

use vantage_core::{Context, Result, error};

pub(crate) use step::{RowWindow, Step};

use crate::{NativeCondition, VistaResolver, vista::Vista};

#[derive(Clone)]
pub struct Handle {
    base: Base,
    steps: Arc<Vec<Step>>,
    /// The resolver `table(name)` was called with, used when a caller
    /// resolves without one.
    resolver: Option<Arc<dyn VistaResolver>>,
    /// The extension verb that last narrowed the base with a native
    /// condition, which [`Handle::query`] can't express.
    native_verb: Option<String>,
}

#[derive(Clone)]
enum Base {
    /// Resolved by name through a [`VistaResolver`].
    Name(String),
    /// A Vista in hand, cloned on each resolve.
    Vista(Arc<Vista>),
}

impl Handle {
    /// A handle on the table `name`, resolved when a terminal reads it.
    pub fn named(name: impl Into<String>) -> Self {
        Self::with_base(Base::Name(name.into()))
    }

    /// A handle over `vista`: the `self` of a `modify:` script.
    pub(crate) fn over(vista: Vista) -> Self {
        Self::with_base(Base::Vista(Arc::new(vista)))
    }

    fn with_base(base: Base) -> Self {
        Self {
            base,
            steps: Arc::new(Vec::new()),
            resolver: None,
            native_verb: None,
        }
    }

    /// The resolver a named base goes through when a caller resolves without
    /// one. A [`RecordDraft`](super::RecordDraft) reads and writes through it.
    pub fn with_resolver(mut self, resolver: Arc<dyn VistaResolver>) -> Self {
        self.resolver = Some(resolver);
        self
    }

    /// The table name this handle starts from; `None` for a handle over a Vista.
    pub fn table_name(&self) -> Option<&str> {
        match &self.base {
            Base::Name(name) => Some(name),
            Base::Vista(_) => None,
        }
    }

    /// A new handle with `step` appended; `self` is left as it was.
    pub(crate) fn push(&self, step: Step) -> Handle {
        let mut steps = Vec::with_capacity(self.steps.len() + 1);
        steps.extend(self.steps.iter().cloned());
        steps.push(step);
        Handle {
            steps: Arc::new(steps),
            ..self.clone()
        }
    }

    /// Build the Vista: the base (resolved by name, or a clone of the Vista
    /// in hand), then every step in order. A named base goes through
    /// `resolver`, else the one `table(name)` was given.
    pub fn resolve(&self, resolver: Option<&dyn VistaResolver>) -> Result<Vista> {
        let mut vista = self.resolve_base(resolver)?;
        let mut ref_window = RowWindow::default();
        for step in self.steps.iter() {
            vista = step
                .apply(vista, ref_window)
                .with_context(|| error!("Step can't be applied", step = step))?;
            ref_window = step.ref_window_after(ref_window);
        }
        Ok(vista)
    }

    /// The Vista writes go to: the whole narrowed handle — `where`, `search`
    /// and `ref` all define the set, so a write only touches rows in it.
    /// `sort` doesn't change membership and is skipped. `limit` and `offset`
    /// define rows by position, not by condition, so a handle with either
    /// can't be written through.
    pub(crate) fn write_target(&self, resolver: Option<&dyn VistaResolver>) -> Result<Vista> {
        if let Some(verb) = self.steps.iter().find_map(Step::positional_verb) {
            return Err(error!(format!(
                "writes need a set defined by conditions; drop {verb}(n)"
            ))
            .mark_incorrect_usage());
        }
        let mut vista = self.resolve_base(resolver)?;
        for step in self.steps.iter() {
            if matches!(step, Step::Sort { .. }) {
                continue;
            }
            vista = step
                .apply(vista, RowWindow::default())
                .with_context(|| error!("Step can't be applied", step = step))?;
        }
        Ok(vista)
    }

    /// The `limit` and `offset` steps after the last `ref` step, folded into
    /// one window.
    pub(crate) fn row_window(&self) -> RowWindow {
        self.steps
            .iter()
            .rev()
            .take_while(|s| !matches!(s, Step::Ref(_)))
            .fold(RowWindow::default(), RowWindow::fold)
    }

    /// A handle whose Vista base is a copy narrowed by `condition`, keeping
    /// its steps; `self` is left as it was. A backend's extension verb
    /// (`verb`, such as `with_condition`) uses this. The base must be a
    /// Vista in hand, so a handle built from `table(name)`, or one past a
    /// `ref`, is an error naming the verb.
    pub fn with_native_condition(&self, verb: &str, condition: NativeCondition) -> Result<Handle> {
        let Base::Vista(source) = &self.base else {
            return Err(error!(
                "Extension verb only works on `self`, not on a handle from `table(...)`",
                verb = verb
            ));
        };
        if self.steps.iter().any(|s| matches!(s, Step::Ref(_))) {
            return Err(error!(
                "Extension verb can't follow `ref(...)`",
                verb = verb
            ));
        }
        let narrowed = Vista::clone(source).with_native_condition(condition)?;
        Ok(Handle {
            base: Base::Vista(Arc::new(narrowed)),
            native_verb: Some(verb.to_string()),
            ..self.clone()
        })
    }

    /// A Vista in hand is adopted by the resolver, so inside `atomic` it comes
    /// back wrapped in the block's transaction.
    fn resolve_base(&self, resolver: Option<&dyn VistaResolver>) -> Result<Vista> {
        let resolver = resolver.or(self.resolver.as_deref());
        match &self.base {
            Base::Name(name) => resolver
                .ok_or_else(|| {
                    error!(
                        "Table can't be resolved here: no tables are available",
                        table = name
                    )
                })?
                .vista(name),
            Base::Vista(vista) => Ok(match resolver {
                Some(resolver) => resolver.adopt(Vista::clone(vista)),
                None => Vista::clone(vista),
            }),
        }
    }
}

#[cfg(all(test, feature = "mocks"))]
mod tests {
    use std::sync::Arc;

    use vantage_dataset::traits::ReadableValueSet;

    use super::Handle;
    use crate::{Transaction, VistaResolver, mocks::MockShell, vista::Vista};

    #[tokio::test]
    async fn a_vista_in_hand_resolves_adopted() {
        let base: Arc<dyn VistaResolver> =
            Arc::new(|name: &str| Ok(Vista::new(name, MockShell::new())));
        let resolver = Transaction::new().resolver(base);
        let handle = Handle::over(Vista::new("a", MockShell::new()));

        let vista = handle.resolve(Some(&*resolver)).unwrap();

        // MockShell can't rebind, so only a wrapped Vista refuses to list.
        let err = vista.list_values().await.unwrap_err();
        assert!(
            err.to_string().contains("can't take part in a transaction"),
            "{err}"
        );
    }
}