explainable 0.1.1

A zero-overhead educational layer for Rust libraries.
Documentation
//! # `explainable`
//!
//! A zero-overhead educational layer for Rust libraries.
//!
//! This crate defines the traits and types that any domain crate can implement
//! to give its users a step-by-step, pedagogical view of an operation chain ---
//! text explanations, before/after visuals, or both --- without touching the
//! domain's hot path.
//!
//! ## Architecture
//!
//! Three interfaces make up the system:
//!
//! | Trait | Implemented by | Purpose |
//! |---|---|---|
//! | [`ExplainDisplay`] | Domain crate | Opaque rendering surface |
//! | [`RenderVisual`] | Domain crate | Produces a before/after [`ExplainDisplay`] |
//! | [`Explainable`] | Domain crate | Opts a type into the system (one line) |
//!
//! The [`#[explainable]`](explainable) attribute macro annotates operation
//! trait definitions in the domain crate. For every method in the trait it
//! generates a matching method on [`Explaining<T>`] that snapshots `inner`
//! before the call, calls through to the real operation, captures after, and
//! accumulates an [`Explanation`].
//!
//! ## Quick start
//!
//! ```rust,ignore
//! // Domain crate --- opt in
//! #[explainable]
//! pub trait MyOperations {
//!     fn some_operation(&self) -> Result<Self, MyError>;
//! }
//!
//! impl MyOperationsExplainText for MyType {
//!     fn explain_text_some_operation(before: &Self, after: &Self) -> String {
//!         format!("Did the thing. Value went from {} to {}.", before, after)
//!     }
//! }
//!
//! impl RenderVisual for MyType { /* ... */ }
//! impl Explainable for MyType {}
//!
//! // User --- one extra call to open an explaining chain
//! use my_crate::MyOperationsExt; // bring the extension trait into scope
//!
//! let (result, _explanations) = value
//!     .explaining(ExplainMode::Both)
//!     .some_operation()
//!     .explain();
//! ```
//!
//! ## Overhead model
//!
//! The hot path is completely unaffected. Explanation machinery activates only
//! when [`.explaining()`][Explainable::explaining] is called. Every existing
//! call site remains valid and untouched.

pub use explainable_macros::explainable;

// ─── Enums ───────────────────────────────────────────────────────────────────

/// Controls which kinds of explanation are produced for each operation in a
/// chain.
///
/// Chosen once when calling [`.explaining()`][Explainable::explaining] and
/// applied uniformly to every operation in that chain.
///
/// # Examples
///
/// ```rust,ignore
/// value.explaining(ExplainMode::Text)    // text only
/// value.explaining(ExplainMode::Visual)  // before/after visuals only
/// value.explaining(ExplainMode::Both)    // text and visuals
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExplainMode {
    /// Produce only textual, pedagogical explanations.
    Text,
    /// Produce only visual before/after representations.
    Visual,
    /// Produce both textual and visual explanations.
    Both,
}

// ─── Traits ──────────────────────────────────────────────────────────────────

/// An opaque rendering surface for a single visual explanation.
///
/// Implemented by the domain crate --- `explainable` never renders directly; it
/// only calls [`display`][ExplainDisplay::display] at the right moment.
///
/// The domain crate supplies a concrete type and
/// implements this trait on it. `explainable` stores it as
/// `Box<dyn ExplainDisplay>` and calls `display` when surfacing explanations.
///
/// # Examples
///
/// ```rust,ignore
/// struct MyVisual { html: String }
///
/// impl ExplainDisplay for MyVisual {
///     fn display(&self) {
///         open_in_browser(&self.html);
///     }
/// }
/// ```
pub trait ExplainDisplay {
    /// Render this explanation to the user using whatever output medium the
    /// domain crate provides (terminal, browser window, GUI canvas, etc.).
    fn display(&self);
}

/// Implemented by a domain type to supply before/after visual explanations.
///
/// This is the single seam through which `explainable` requests a visual
/// without knowing anything about how it is produced. The domain crate owns
/// its rendering infrastructure entirely.
///
/// # Examples
///
/// ```rust,ignore
/// impl RenderVisual for MyType {
///     fn render_visual(before: &Self, after: &Self) -> Box<dyn ExplainDisplay> {
///         Box::new(MyVisual { html: plot_before_after(before, after) })
///     }
/// }
/// ```
pub trait RenderVisual: Clone {
    /// Produce a visual explanation comparing the state of the value
    /// `before` and `after` an operation.
    fn render_visual(before: &Self, after: &Self) -> Box<dyn ExplainDisplay>;
}

/// Marker trait that opts a type into the explaining system.
///
/// Any type that is [`Clone`] and implements [`RenderVisual`] can implement
/// this trait. No body is required --- the default implementation of
/// [`explaining`][Explainable::explaining] is provided automatically.
///
/// # Examples
///
/// ```rust,ignore
/// // One line --- the macro and the two trait impls do the rest.
/// impl Explainable for MyType {}
/// ```
pub trait Explainable: Clone + RenderVisual {
    /// Wrap `self` in an [`Explaining`] context, selecting which kinds of
    /// explanation will be produced for every operation in the chain.
    ///
    /// Call [`.explain()`][Explaining::explain] at the end of the chain to
    /// surface all accumulated explanations and recover the final value.
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// let (result, explanations) = value
    ///     .explaining(ExplainMode::Both)
    ///     .some_operation()
    ///     .explain();
    /// ```
    #[must_use = "call .explain() at the end of the chain to surface explanations and recover the value"]
    fn explaining(&self, mode: ExplainMode) -> Explaining<Self> {
        Explaining {
            inner: self.clone(),
            mode,
            explanations: Vec::new(),
        }
    }
}

// ─── Data types ──────────────────────────────────────────────────────────────

/// A single accumulated explanation for one operation in an explaining chain.
///
/// Each field is optional --- which are populated depends on the [`ExplainMode`]
/// chosen at the start of the chain. Proc-macro-generated wrapper methods on
/// [`Explaining`] construct these and push them onto the explanation list.
///
/// Instances are returned inside the [`Vec<Explanation>`] from
/// [`Explaining::explain`], giving callers the ability to inspect or
/// re-surface them after the fact via [`surface`][Explanation::surface].
pub struct Explanation {
    /// The mode that was active when this explanation was created.
    pub mode: ExplainMode,
    /// Pedagogical text for this operation, or `None` when the active mode
    /// does not include [`ExplainMode::Text`].
    pub text: Option<String>,
    /// Visual before/after representation, or `None` when the active mode
    /// does not include [`ExplainMode::Visual`].
    pub visual: Option<Box<dyn ExplainDisplay>>,
}

impl Explanation {
    /// Construct a new `Explanation`.
    ///
    /// This is the constructor used by proc-macro-generated code in domain
    /// crates. Direct struct-literal construction is equally valid since all
    /// fields are `pub`.
    pub fn new(
        mode: ExplainMode,
        text: Option<String>,
        visual: Option<Box<dyn ExplainDisplay>>,
    ) -> Self {
        Self { mode, text, visual }
    }

    /// Surface this explanation: print text to stdout and/or invoke the visual
    /// renderer, according to the active [`ExplainMode`].
    ///
    /// Called automatically by [`Explaining::explain`] for every accumulated
    /// explanation in the chain. Also available directly for callers who hold
    /// a [`Vec<Explanation>`] and wish to re-surface explanations later.
    pub fn surface(&self) {
        if let Some(t) = &self.text
            && matches!(self.mode, ExplainMode::Text | ExplainMode::Both)
        {
            println!("{}", t);
        }
        if let Some(v) = &self.visual
            && matches!(self.mode, ExplainMode::Visual | ExplainMode::Both)
        {
            v.display();
        }
    }
}

/// An explaining chain wrapping a value of type `T`.
///
/// Created by calling [`.explaining(mode)`][Explainable::explaining] on any
/// type that implements [`Explainable`]. Methods generated by the
/// `#[explainable]` proc-macro are available on this wrapper --- each delegates
/// to the real operation on `inner`, captures a before/after [`Explanation`],
/// and returns `&mut Self` for method chaining.
///
/// Call [`.explain()`][Explaining::explain] at the end of the chain to surface
/// all accumulated explanations and recover the final value.
///
/// # Examples
///
/// ```rust,ignore
/// let (result, explanations) = value
///     .explaining(ExplainMode::Both)
///     .normalize()
///     .scale(0.5)
///     .trim(100, 200)
///     .explain();
/// ```
#[must_use = "call .explain() to surface explanations and recover the final value"]
pub struct Explaining<T: Explainable> {
    /// The wrapped value. Read and mutated by proc-macro-generated operation
    /// methods.
    pub inner: T,
    /// The active explanation mode for this chain.
    pub mode: ExplainMode,
    /// Explanations accumulated so far, one per operation called on this
    /// chain, in call order.
    pub explanations: Vec<Explanation>,
}

impl<T: Explainable> Explaining<T> {
    /// Finish the chain, surface all accumulated explanations in order, and
    /// return the final value together with the full explanation record.
    ///
    /// Takes `&mut self` so it can be called at the end of a `&mut Self` builder
    /// chain (the pattern produced by the `#[explainable]` extension methods).
    /// The inner value is cloned out; the explanations list is drained.
    ///
    /// Text explanations are printed to stdout; visuals are rendered via the
    /// domain crate's [`ExplainDisplay`] implementation.
    ///
    /// # Returns
    ///
    /// A `(final_value, explanations)` tuple. The [`Vec<Explanation>`] can be
    /// inspected, stored, or re-surfaced later via [`Explanation::surface`].
    #[must_use = "the first element is the result of the operation chain"]
    pub fn explain(&mut self) -> (T, Vec<Explanation>) {
        let explanations = std::mem::take(&mut self.explanations);
        for exp in &explanations {
            exp.surface();
        }
        (self.inner.clone(), explanations)
    }
}