lernie 0.1.56

lernie: the operator seat — the window and wire client for a yog server
Documentation
//! **A gesture that names no workspace, asked of every channel this box
//! holds** — the union, stamped with where each answer came from.
//!
//! The window has always done this: REMOTE §8.2's window half is *"the union
//! across those slices, local first and then the entries in leaf order, every
//! row carrying the channel it came from"*. The CLI's shorthand for the same
//! question did not, and the two surfaces disagreed on one box (bl-0d54) —
//! `lernie workspaces` answered this box's own engine and said nothing about
//! the rest, so a laptop that holds no engine of its own and is a client of a
//! server elsewhere was told nothing was provisioned, while its own window
//! painted the server's walls.
//!
//! **Which gestures fan is read off [`crate::verbs`]'s one table**, never
//! listed here: a verb with no `workspace` parameter has no way to name a
//! channel, so its subject is all of them ([`crate::verbs::Verb::addresses_a_workspace`]).
//!
//! **Nothing is rewritten on the way out.** §8.2's leaf↔host-name mapping is
//! spent at exactly one place, [`super::route`], and there is nothing to spend
//! here: the envelope names no workspace, so no entry's rename can apply to it.
//!
//! **A refusal is one channel's, never the set's** — the same rule the entries
//! directory has carried since §8.2. A channel that will not open or will not
//! answer says so in its own section while its neighbours answer; only a fan
//! that learned nothing at all is a failure, because only then was the question
//! unanswered.

use std::path::Path;

use serde_json::Value;

use super::{OWN, holds, said};
use crate::channel::entries;
use crate::channel::material::{self, Whose};
use crate::cli::Verdict;
use crate::envelope;
use crate::render::Form;

/// Ask `envelope` of every channel this box holds and answer with the union.
///
/// Each section is a channel's name as the roster spells it, then that
/// channel's own answer indented under it — the shape [`super::listing`]
/// already prints, because this is that listing *answered*.
pub fn fanned(data_root: &Path, envelope: &Value, form: Form) -> Verdict {
    let seated: Vec<(String, Result<crate::channel::Channel, String>)> =
        entries::read_dir(&entries::dir(data_root))
            .into_iter()
            .map(|held| (holds::label(&held.leaf, &held.workspace), held.open()))
            .collect();
    let asked: Vec<(String, Result<Vec<Value>, String>)> = own(data_root, &seated)
        .into_iter()
        .chain(seated)
        .map(|(name, channel)| {
            let said = channel
                .map_err(crate::channel::Reach::Unsent)
                .and_then(|open| open.ask(envelope))
                .map_err(|reach| reach.said());
            (name, said)
        })
        .collect();
    let answered = asked.iter().any(|(_, said)| {
        said.as_ref()
            .is_ok_and(|stream| envelope::succeeded(stream))
    });
    let text = asked
        .iter()
        .map(|(name, answer)| section(name, answer, form))
        .collect::<Vec<String>>()
        .join("\n");
    Verdict::answered(text, answered)
}

/// **This box's own engine, where it is a channel this box HOLDS** — and
/// where it is all this box could hold (bl-b858).
///
/// An unprovisioned flat root is an ABSENCE, not a fault. On a seat for an
/// engine somewhere else — a box that holds named entries and no engine of its
/// own, provisioned exactly as `enroll --into` instructs — enumerating it put a
/// paragraph of refusal above every working channel on every roster read,
/// beginning *"if an engine runs on this box"* on a box where none does and
/// none is meant to. The read had worked; the answer was under the complaint.
///
/// **One condition rather than a special case**, and it is the same sentence
/// read twice: the bare channel is enumerated when it is provisioned, or when
/// it is the only thing there is. A box with nothing at all still gets the
/// refusal and its whole recipe — that is precisely the box the recipe is for,
/// and silence there would answer an empty fan with nothing.
///
/// **Provisioned is not the same as reachable, and only the first is asked
/// here.** Half-provisioned material, a `:0` address, a channel that will not
/// open: each is a fault about a channel this box does have, and each still
/// says so in its own section. What is dropped is the one reading that is not
/// about a channel at all.
fn own(
    data_root: &Path,
    seated: &[(String, Result<crate::channel::Channel, String>)],
) -> Option<(String, Result<crate::channel::Channel, String>)> {
    let held = material::read_dir(&entries::flat(data_root), Whose::Own);
    (!matches!(held, Ok(None)) || seated.is_empty())
        .then(|| (OWN.to_owned(), super::route::flat(data_root)))
}

/// One channel's section: its name, then what it said, indented under it.
///
/// The channel stamp is **this box's**, exactly as the window's is — no origin
/// crosses the wire and no reply grew a field the engine cannot fill.
fn section(name: &str, answer: &Result<Vec<Value>, String>, form: Form) -> String {
    let body = match answer {
        Ok(stream) => said(stream, form),
        Err(refusal) => refusal.clone(),
    };
    std::iter::once(name.to_owned())
        .chain(body.lines().map(|line| format!("    {line}")))
        .collect::<Vec<String>>()
        .join("\n")
}

#[cfg(test)]
mod tests;