git-ents.gitmain
⌘K
foforge
commit 4a3d047
feat: render meta-ref documents through a Facet-driven Render trait

The Checks and Settings cards each hand-wrote the same row markup for their meta-ref documents. A new Render trait, bounded on Facet, gives a default that walks any value’s reflected shape — struct, map, list, or scalar — into card rows, so a new refs/meta/* type renders with no extra code. Check uses the default; Signer and Run override render to keep the shortened key label and the one-line run summary. The pages iterate the typed sets and call render per item, keeping card chrome in place.

feat: add web::render with a Facet-default Render trait and Signer/Run overrides feat: derive Facet on the public Check, Signer, Run, and RunOutcome types refactor: render the Checks and Authorized-signers cards via Render build: add facet to git-ents-server deps Assisted-by: Claude:claude-opus-4-8

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

Cargo.lock @@ -1039,6 +1039,7 @@ "axum", "clap", "clap_mangen", + "facet", "git-ents", "gix-actor", "gix-date",
crates/git-ents-server/Cargo.toml @@ -14,6 +14,7 @@ axum = { workspace = true } clap = { workspace = true } clap_mangen = { workspace = true } +facet = { workspace = true } gix-actor = { workspace = true } gix-date = { workspace = true } gix-hash = { workspace = true }
crates/git-ents/src/checks.rs @@ -33,7 +33,7 @@ } /// One configured check recorded in [`CHECKS_REF`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Check { /// The name it is stored under. pub name: String, @@ -104,7 +104,7 @@ } /// One check's outcome within a [`Run`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct RunOutcome { /// The check's name (its `checks/<name>` in [`CHECKS_REF`]). pub name: String, @@ -114,7 +114,7 @@ } /// One recorded execution of the check set against a commit. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Run { /// When the run was recorded, as seconds since the Unix epoch — the run /// commit's committer date.
crates/git-ents/src/signers.rs @@ -22,7 +22,7 @@ } /// One authorized signer recorded in [`AUTH_REF`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Signer { /// The key it is stored under — its fingerprint. pub fingerprint: String,
crates/git-ents-server/src/web/mod.rs @@ -4,13 +4,15 @@ //! smart-HTTP gateway in [`crate::http`] delegates plain browser GETs here. //! //! The module is split by concern: [`assets`] bundles the CSS/JS, [`icons`] -//! holds the inline SVGs, [`git`] is the data layer over `git`, and [`pages`] -//! renders each tab. This file owns routing and the shared page shell. +//! holds the inline SVGs, [`git`] is the data layer over `git`, [`render`] +//! turns reflected meta-ref values into HTML, and [`pages`] renders each tab. +//! This file owns routing and the shared page shell. mod assets; mod git; mod icons; mod pages; +mod render; use std::path::{Path, PathBuf};
crates/git-ents-server/src/web/pages.rs @@ -20,6 +20,7 @@ releases, root_tree, }; use super::icons::*; +use super::render::Render; use super::{RepoMeta, Tab, not_found, repo_shell}; /// A single repository's overview: the rendered README beside an aside of @@ -658,7 +659,7 @@ @for run in &commit.runs { div.card-row.signer-row { code.key { (commit.commit.get(..8).unwrap_or(&commit.commit)) } - span.muted { (run_summary(run)) } + (run.render()) } } } @@ -679,10 +680,7 @@ } Ok(checks) => { @for check in checks { - div.card-row.signer-row { - code.key { (check.name) } - span.muted { (check.command) } - } + (check.render()) } } } @@ -711,15 +709,6 @@ .map_err(|err| err.to_string()) } -/// A one-line summary of a run's outcomes, e.g. `fmt pass · test fail`. -fn run_summary(run: &git_ents::checks::Run) -> String { - run.results - .iter() - .map(|result| format!("{} {}", result.name, result.outcome)) - .collect::<Vec<_>>() - .join(" · ") -} - /// The Issues ("Bug reports") tab. There is no issue store yet, so the filters /// are present for the design and the list is an empty state. pub(super) fn issues_page(meta: &RepoMeta) -> Markup { @@ -813,10 +802,7 @@ } Ok(signers) => { @for signer in signers { - div.card-row.signer-row { - code.key { (signer.fingerprint) } - span.muted { (signer_label(&signer.key)) } - } + (signer.render()) } } } @@ -860,19 +846,6 @@ .map_err(|err| err.to_string()) } -/// A short label for a signer's key: its type and trailing comment, dropping the -/// long base64 body that would not fit on the row. -fn signer_label(key: &str) -> String { - let mut parts = key.split_whitespace(); - let kind = parts.next().unwrap_or_default(); - let comment = parts.nth(1).unwrap_or_default(); - if comment.is_empty() { - kind.to_owned() - } else { - format!("{kind} · {comment}") - } -} - /// A Features row with a static toggle reflecting `on`. fn feature_row(title: &str, desc: &str, on: bool) -> Markup { html! {
crates/git-ents-server/src/web/render.rs @@ -1,0 +1,126 @@ +//! Turning typed `refs/meta/*` documents into HTML. +//! +//! Every value the meta refs carry is a [`facet::Facet`] type, so one structural +//! walk over its reflected shape can render any of them: a struct becomes a row +//! (its first field the key, the rest a muted value), a map becomes one keyed +//! row per entry, a list stacks its items, and a scalar is its text. That walk +//! is the [`Render`] trait's default, so a new meta-ref type renders for free. +//! Types whose presentation needs domain knowledge — a signer's shortened key, a +//! run's one-line summary — override [`Render::render`] instead. + +use facet::{Def, Facet, Peek, Type, UserType}; +use maud::{Markup, html}; + +use git_ents::checks::{Check, Run}; +use git_ents::signers::Signer; + +/// HTML rendering for a meta-ref value. The default walks the value's [`Facet`] +/// shape structurally; a type overrides [`render`](Render::render) when its +/// presentation needs more than the structure carries. +pub(super) trait Render: for<'a> Facet<'a> { + /// Render `self` as a fragment of card rows. + fn render(&self) -> Markup { + render_peek(Peek::new(self)) + } +} + +/// A check renders structurally: its name is the key, its command the value. +impl Render for Check {} + +/// A signer's stored key is too long for a row, so show a short label beside the +/// fingerprint instead of the raw key the structural walk would print. +impl Render for Signer { + fn render(&self) -> Markup { + row(&self.fingerprint, &signer_label(&self.key)) + } +} + +/// A run is a set of per-check outcomes; collapse them to one summary line +/// rather than a row per outcome. +impl Render for Run { + fn render(&self) -> Markup { + html! { span.muted { (run_summary(self)) } } + } +} + +/// Render any reflected value as card rows by walking its shape. +fn render_peek(peek: Peek<'_, '_>) -> Markup { + match peek.shape().def { + Def::Scalar => return html! { (scalar_text(&peek)) }, + Def::Map(_) => { + let Ok(map) = peek.into_map() else { + return html! {}; + }; + return html! { + @for (key, value) in map.iter() { + (row(&scalar_text(&key), &scalar_text(&value))) + } + }; + } + Def::List(_) | Def::Array(_) | Def::Slice(_) => { + let Ok(list) = peek.into_list_like() else { + return html! {}; + }; + return html! { @for item in list.iter() { (render_peek(item)) } }; + } + _ => {} + } + if let Type::User(UserType::Struct(st)) = peek.shape().ty { + let Ok(strukt) = peek.into_struct() else { + return html! {}; + }; + let mut key = String::new(); + let mut rest: Vec<String> = Vec::new(); + for index in 0..st.fields.len() { + let Ok(field) = strukt.field(index) else { + continue; + }; + let text = scalar_text(&field); + if index == 0 { + key = text; + } else { + rest.push(text); + } + } + return row(&key, &rest.join(" · ")); + } + html! {} +} + +/// One card row: a key in `code.key`, its value muted beside it. +fn row(key: &str, value: &str) -> Markup { + html! { + div.card-row.signer-row { + code.key { (key) } + span.muted { (value) } + } + } +} + +/// The textual form of a scalar peek: its string value, or its `Display`. +fn scalar_text(peek: &Peek<'_, '_>) -> String { + peek.as_str() + .map_or_else(|| format!("{peek}"), str::to_owned) +} + +/// A short label for a signer's key: its type and trailing comment, dropping the +/// long base64 body that would not fit on the row. +fn signer_label(key: &str) -> String { + let mut parts = key.split_whitespace(); + let kind = parts.next().unwrap_or_default(); + let comment = parts.nth(1).unwrap_or_default(); + if comment.is_empty() { + kind.to_owned() + } else { + format!("{kind} · {comment}") + } +} + +/// A one-line summary of a run's outcomes, e.g. `fmt pass · test fail`. +fn run_summary(run: &Run) -> String { + run.results + .iter() + .map(|result| format!("{} {}", result.name, result.outcome)) + .collect::<Vec<_>>() + .join(" · ") +}