Skip to main content

dsp_cli/render/
mod.rs

1//! Renderer layer (3b of ADR-0008) — output formatting.
2//!
3//! The `Renderer` trait has explicit per-noun methods (prose is irreducibly
4//! per-noun); each format impl is a separate struct (prose, json, lines,
5//! csv, tsv). `MetaContext` threads the auth-state disclosure from ADR-0007
6//! through every call.
7//!
8//! `Format` is the user-facing enum that maps `--format` flag values to
9//! concrete renderer instances via `Format::into_renderer`. It derives
10//! `clap::ValueEnum` so the CLI can parse it directly.
11
12pub mod auth;
13pub mod csv;
14pub mod dump;
15pub mod format;
16pub mod json;
17pub mod lines;
18pub mod progress;
19pub mod prose;
20pub(crate) mod table;
21#[cfg(test)]
22mod test_support;
23pub mod tsv;
24pub(crate) mod value;
25
26pub use auth::{AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome};
27pub use dump::{DumpDeleteOutcome, DumpEvent, DumpOutcome};
28pub use format::Format;
29pub use progress::{HumanProgress, JsonProgress, ProgressReporter};
30// Re-exported for plan 020 steps 2–5: renderer methods, engine error hints,
31// and CLI `after_help` drift-guard tests.
32pub(crate) use table::{
33    AUTH_LOGIN_COLUMNS, AUTH_LOGOUT_COLUMNS, DATA_MODEL_DESCRIBE_COLUMNS,
34    DATA_MODEL_STRUCTURE_COLUMNS, DATA_MODELS_COLUMNS, PROJECT_DUMP_COLUMNS,
35    PROJECT_DUMP_DELETED_COLUMNS, PROJECTS_COLUMNS, QuoteMode, RESOURCE_DESCRIBE_COLUMNS,
36    RESOURCE_DESCRIBE_VALUES_COLUMNS, RESOURCE_DESCRIBE_VALUES_DEFAULT_COLUMNS,
37    RESOURCE_LIST_COLUMNS, RESOURCE_TYPE_DESCRIBE_COLUMNS, RESOURCE_TYPE_DESCRIBE_DEFAULT_COLUMNS,
38    RESOURCE_TYPES_COLUMNS, TableSpec, render_table,
39};
40// TableOptions and HeaderMode are public: integration tests in `tests/` construct
41// renderers with specific options via `with_options`. The column-set consts are
42// pub(crate) only (used in renderer bodies and the engine error hints, not the
43// test API).
44//
45// `with_options` is `pub` (not `pub(crate)`) because integration tests are a
46// separate crate and need to call it to exercise the new --columns and header
47// flags end-to-end; bypassing FormatArgs::table_options validation via
48// with_options is an internal/test-only concern that does not affect the
49// production dispatch path.
50pub use table::{HeaderMode, TableOptions};
51
52use crate::diagnostic::Diagnostic;
53use crate::model::{
54    DataModel, DataModelDetail, DataModelStructure, Project, ProjectDetail, ResourceDetail,
55    ResourceSummary, ResourceType, ResourceTypeDetail,
56};
57
58/// The data a renderer needs to render a `project list` result.
59///
60/// `total` is the pre-filter count; `filter` is the applied substring (if any),
61/// used only by prose for the "(m of n matching "…")" count line.
62///
63/// Owns its data (no borrow/lifetime): it is a per-call view, never stored, and
64/// the action builds it by moving the already-sorted `Vec<Project>` in. Owning
65/// avoids a `'a` parameter leaking into the `Renderer::projects` signature (and
66/// the latent lifetime-elision trap a future caching renderer would hit). The
67/// clone cost is nil — the vec is moved, not copied.
68#[derive(Debug)]
69pub struct ProjectListView {
70    pub items: Vec<Project>,
71    /// Pre-filter total count (before `--filter` was applied). Feeds the
72    /// "(m of total matching …)" prose count line.
73    pub total: usize,
74    /// The `--filter` substring the user supplied, if any.
75    pub filter: Option<String>,
76}
77
78/// The data a renderer needs to render a `data-model list` result.
79///
80/// `total` is the pre-filter count (it includes any built-ins the action
81/// appended); `filter` the applied substring (if any). Whether built-ins are
82/// present is read off the items themselves (`is_builtin`), so it is not carried
83/// as a separate field.
84#[derive(Debug, Clone)]
85pub struct DataModelListView {
86    pub items: Vec<DataModel>,
87    /// Pre-filter total count (before `--filter` was applied). Feeds the
88    /// "(m of total matching …)" prose count line.
89    pub total: usize,
90    /// The `--filter` substring the user supplied, if any.
91    pub filter: Option<String>,
92}
93
94/// The data a renderer needs to render a `resource-type list` result.
95///
96/// `items` is the post-filter, sorted list; `total` is the pre-filter count
97/// (after any built-ins were appended, before `--filter` was applied); `filter`
98/// is the applied substring (if any). Whether built-ins are present is read off
99/// the items themselves (`is_builtin`), so it is not carried as a separate field.
100///
101/// `data_model` carries the resolved parent data-model's **name** for the prose
102/// header (`resource-types in <data_model> on <server>`). This is a deliberate
103/// extension beyond `ProjectListView`/`DataModelListView` (which carry no parent):
104/// resource-type list is sub-scoped to one data-model, and the name must appear
105/// even when `items` is empty (so it cannot be derived from `items[0].iri`).
106/// Threading it through `MetaContext` was rejected — that struct is auth/server
107/// disclosure only (ADR-0007), so overloading it is a worse coupling than this
108/// explicit field. Tabular and JSON renderers ignore `data_model`.
109///
110/// `Clone` is required because the test `RecordingRenderer` stores the view in
111/// an `Option<ResourceTypeListView>`.
112#[derive(Debug, Clone)]
113pub struct ResourceTypeListView {
114    pub items: Vec<ResourceType>,
115    /// Pre-filter total count (after built-ins were appended, before `--filter`
116    /// was applied). Feeds the "(m of total matching …)" prose count line.
117    pub total: usize,
118    /// The `--filter` substring the user supplied, if any.
119    pub filter: Option<String>,
120    /// The resolved parent data-model's NAME, for the prose header. Tabular/json
121    /// ignore it. See struct-level doc for why this field exists.
122    pub data_model: String,
123}
124
125/// Pagination state for a `resource list` render call (D5 of the plan).
126///
127/// Models two mutually exclusive modes as an enum rather than two loose
128/// `Option<u32>` fields that admit invalid combinations (e.g. both set).
129/// The json renderer matches on this to build `_meta` pagination keys;
130/// prose reads `may_have_more` (SinglePage only) for the "use --all" hint.
131///
132/// `AllPages` hard-codes `may_have_more_results: false` because the `--all`
133/// loop only exits when the server reports `false` — the variant carries no
134/// flag because it is guaranteed.
135#[derive(Debug, Clone)]
136pub enum ResourceListPagination {
137    /// A single page was fetched (default or `--page N`).
138    SinglePage {
139        /// The page number that was fetched (zero-based).
140        page: u32,
141        /// Whether the server reported more pages after this one.
142        may_have_more: bool,
143    },
144    /// All pages were fetched (`--all`).
145    AllPages {
146        /// Total number of pages fetched.
147        pages_fetched: u32,
148    },
149}
150
151/// The data a renderer needs to render a `resource list` result.
152///
153/// `items` is the post-filter list; `total` is the pre-filter count (capture
154/// BEFORE applying `--filter`, mirroring `project list`); `filter` is the
155/// applied substring (if any). `resource_type` carries the resolved class name
156/// for the prose header. `pagination` carries the D5 mode struct (single page
157/// vs. all pages).
158///
159/// `Clone` is required because the test `RecordingRenderer` stores the view in
160/// an `Option<ResourceListView>`.
161#[derive(Debug, Clone)]
162pub struct ResourceListView {
163    /// Post-filter, sorted resource summaries.
164    pub items: Vec<ResourceSummary>,
165    /// Pre-filter total count. Feeds the "(m of total matching "…")" prose line.
166    pub total: usize,
167    /// The `--filter` substring the user supplied, if any.
168    pub filter: Option<String>,
169    /// Local name of the resource type, for the prose header.
170    pub resource_type: String,
171    /// Pagination state (single page vs. all-pages drain).
172    pub pagination: ResourceListPagination,
173}
174
175/// Auth and server context attached to every rendered response.
176/// See ADR-0007.
177#[derive(Debug, Clone)]
178pub struct MetaContext {
179    pub server_label: String,
180    pub auth_state: String,
181    /// ADR-0007 silent-filter disclosure for instance-side reads.
182    ///
183    /// Set to `Some(message)` by instance-side commands (`resource list`,
184    /// `resource describe`) to disclose that results may be filtered by the
185    /// caller's authentication state (anonymous → only public resources visible;
186    /// authenticated → bounded by permissions). `None` for schema-side commands
187    /// (`project list/describe`, `data-model list/describe`, `resource-type
188    /// list/describe`, `data-model structure`) that are not affected by caller
189    /// identity.
190    pub filter_warning: Option<String>,
191}
192
193/// The `Renderer` trait. Methods grow as new noun-groups land.
194///
195/// All methods return `Result<(), Diagnostic>`. I/O errors in renderer impls
196/// are converted via `impl From<std::io::Error> for Diagnostic`, which means
197/// `writeln!(self.out, "...")?;` Just Works against this return type.
198pub trait Renderer {
199    /// Render a diagnostic in the appropriate shape for this format.
200    fn diagnostic(&mut self, diag: &Diagnostic, meta: &MetaContext) -> Result<(), Diagnostic>;
201
202    /// Render a successful `dsp auth login` outcome.
203    fn auth_login(
204        &mut self,
205        outcome: &AuthLoginOutcome,
206        meta: &MetaContext,
207    ) -> Result<(), Diagnostic>;
208
209    /// Render a `dsp auth status` outcome (logged-in or not-logged-in).
210    fn auth_status(
211        &mut self,
212        outcome: &AuthStatusOutcome,
213        meta: &MetaContext,
214    ) -> Result<(), Diagnostic>;
215
216    /// Render a `dsp auth logout` outcome.
217    fn auth_logout(
218        &mut self,
219        outcome: &AuthLogoutOutcome,
220        meta: &MetaContext,
221    ) -> Result<(), Diagnostic>;
222
223    /// Render a successful `dsp auth set-token` outcome.
224    fn auth_set_token(
225        &mut self,
226        outcome: &AuthSetTokenOutcome,
227        meta: &MetaContext,
228    ) -> Result<(), Diagnostic>;
229
230    /// Render a `dsp vre project dump` outcome.
231    fn project_dump(&mut self, outcome: &DumpOutcome, meta: &MetaContext)
232    -> Result<(), Diagnostic>;
233
234    /// Render a `dsp vre project dump --delete` outcome.
235    ///
236    /// `outcome.deleted = false` means no completed/failed dump existed and a
237    /// probe created an in-progress dump — NOT a delete failure (failures are
238    /// `Err(Diagnostic)`).
239    fn project_dump_deleted(
240        &mut self,
241        outcome: &DumpDeleteOutcome,
242        meta: &MetaContext,
243    ) -> Result<(), Diagnostic>;
244
245    /// Render a `dsp vre project list` result (possibly empty).
246    ///
247    /// `view` carries the items (post-filter, sorted), the pre-filter total,
248    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
249    fn projects(&mut self, view: &ProjectListView, meta: &MetaContext) -> Result<(), Diagnostic>;
250
251    /// Render a `dsp vre project describe` result (a single project).
252    ///
253    /// `project` is passed directly — no view wrapper, since there is no
254    /// aggregate context (no `total`/`filter`) for a single-object describe.
255    /// `meta` carries auth/server disclosure per ADR-0007.
256    fn project_describe(
257        &mut self,
258        project: &ProjectDetail,
259        meta: &MetaContext,
260    ) -> Result<(), Diagnostic>;
261
262    /// Render a `dsp vre data-model list` result (possibly empty).
263    ///
264    /// `view` carries the items (post-filter, sorted), the pre-filter total,
265    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
266    fn data_models(
267        &mut self,
268        view: &DataModelListView,
269        meta: &MetaContext,
270    ) -> Result<(), Diagnostic>;
271
272    /// Render a `dsp vre data-model describe` result (a single data-model).
273    ///
274    /// `detail` is passed directly — no view wrapper, since there is no aggregate
275    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
276    /// auth/server disclosure per ADR-0007.
277    fn data_model_describe(
278        &mut self,
279        detail: &DataModelDetail,
280        meta: &MetaContext,
281    ) -> Result<(), Diagnostic>;
282
283    /// Render a `dsp vre resource-type list` result (possibly empty).
284    ///
285    /// `view` carries the items (post-filter, sorted), the pre-filter total, the
286    /// filter string, and the parent data-model name. `meta` carries auth/server
287    /// disclosure (ADR-0007).
288    fn resource_types(
289        &mut self,
290        view: &ResourceTypeListView,
291        meta: &MetaContext,
292    ) -> Result<(), Diagnostic>;
293
294    /// Render a `dsp vre resource-type describe` result (a single resource-type).
295    ///
296    /// `detail` is passed directly — no view wrapper, since there is no aggregate
297    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
298    /// auth/server disclosure per ADR-0007. Built-in field filtering is applied by
299    /// the action (via `--include-builtins`) before this method is called — the
300    /// renderer receives only the fields it should render.
301    fn resource_type_describe(
302        &mut self,
303        detail: &ResourceTypeDetail,
304        meta: &MetaContext,
305    ) -> Result<(), Diagnostic>;
306
307    /// Render a `dsp vre data-model structure` result (a single data-model's relations).
308    ///
309    /// `structure` is passed directly — no view wrapper (describe-shaped, like
310    /// `data_model_describe`). Built-in relation filtering via `--include-builtins`
311    /// is applied by the action before this method is called; the renderer receives
312    /// only the relations it should render. `meta` carries auth/server disclosure
313    /// per ADR-0007.
314    fn data_model_structure(
315        &mut self,
316        structure: &DataModelStructure,
317        meta: &MetaContext,
318    ) -> Result<(), Diagnostic>;
319
320    /// Render a `dsp vre resource list` result (possibly empty).
321    ///
322    /// `view` carries the items (post-filter), the pre-filter total, the filter
323    /// string, the resource type name, and the pagination state. `meta` carries
324    /// auth/server disclosure (ADR-0007) including the always-present
325    /// `filter_warning` for instance-side commands (D3).
326    fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic>;
327
328    /// Render a `dsp vre resource describe` result (a single resource's envelope).
329    ///
330    /// `detail` is passed directly — no view wrapper, since there is no aggregate
331    /// context for a single-object describe. `meta` carries auth/server disclosure
332    /// per ADR-0007 including the always-present `filter_warning` for instance-side
333    /// commands (D3).
334    fn resource_describe(
335        &mut self,
336        detail: &ResourceDetail,
337        meta: &MetaContext,
338    ) -> Result<(), Diagnostic>;
339}