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, RESOURCE_TYPES_DEFAULT_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    /// Schema-side `--count` disclosure note (`resource-type list`/`describe`).
192    ///
193    /// Set to `Some(message)` by the action layer when `--count` was passed,
194    /// disclosing that the v3 `resourcesPerOntology` counts are NOT
195    /// permission-filtered (unlike `resource list`'s `filter_warning`, which IS
196    /// permission-filtered) and exclude deleted resources. `None` when `--count`
197    /// was not used, and always `None` for every command other than
198    /// resource-type list/describe. Deliberately a DISTINCT field from
199    /// `filter_warning` — a different semantic contract, not reused (see plan
200    /// docs/design/plans/030-resource-type-count/implementation-plan.md).
201    pub count_caveat: Option<String>,
202}
203
204/// The `Renderer` trait. Methods grow as new noun-groups land.
205///
206/// All methods return `Result<(), Diagnostic>`. I/O errors in renderer impls
207/// are converted via `impl From<std::io::Error> for Diagnostic`, which means
208/// `writeln!(self.out, "...")?;` Just Works against this return type.
209pub trait Renderer {
210    /// Render a diagnostic in the appropriate shape for this format.
211    fn diagnostic(&mut self, diag: &Diagnostic, meta: &MetaContext) -> Result<(), Diagnostic>;
212
213    /// Render a successful `dsp auth login` outcome.
214    fn auth_login(
215        &mut self,
216        outcome: &AuthLoginOutcome,
217        meta: &MetaContext,
218    ) -> Result<(), Diagnostic>;
219
220    /// Render a `dsp auth status` outcome (logged-in or not-logged-in).
221    fn auth_status(
222        &mut self,
223        outcome: &AuthStatusOutcome,
224        meta: &MetaContext,
225    ) -> Result<(), Diagnostic>;
226
227    /// Render a `dsp auth logout` outcome.
228    fn auth_logout(
229        &mut self,
230        outcome: &AuthLogoutOutcome,
231        meta: &MetaContext,
232    ) -> Result<(), Diagnostic>;
233
234    /// Render a successful `dsp auth set-token` outcome.
235    fn auth_set_token(
236        &mut self,
237        outcome: &AuthSetTokenOutcome,
238        meta: &MetaContext,
239    ) -> Result<(), Diagnostic>;
240
241    /// Render a `dsp vre project dump` outcome.
242    fn project_dump(&mut self, outcome: &DumpOutcome, meta: &MetaContext)
243    -> Result<(), Diagnostic>;
244
245    /// Render a `dsp vre project dump --delete` outcome.
246    ///
247    /// `outcome.deleted = false` means no completed/failed dump existed and a
248    /// probe created an in-progress dump — NOT a delete failure (failures are
249    /// `Err(Diagnostic)`).
250    fn project_dump_deleted(
251        &mut self,
252        outcome: &DumpDeleteOutcome,
253        meta: &MetaContext,
254    ) -> Result<(), Diagnostic>;
255
256    /// Render a `dsp vre project list` result (possibly empty).
257    ///
258    /// `view` carries the items (post-filter, sorted), the pre-filter total,
259    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
260    fn projects(&mut self, view: &ProjectListView, meta: &MetaContext) -> Result<(), Diagnostic>;
261
262    /// Render a `dsp vre project describe` result (a single project).
263    ///
264    /// `project` is passed directly — no view wrapper, since there is no
265    /// aggregate context (no `total`/`filter`) for a single-object describe.
266    /// `meta` carries auth/server disclosure per ADR-0007.
267    fn project_describe(
268        &mut self,
269        project: &ProjectDetail,
270        meta: &MetaContext,
271    ) -> Result<(), Diagnostic>;
272
273    /// Render a `dsp vre data-model list` result (possibly empty).
274    ///
275    /// `view` carries the items (post-filter, sorted), the pre-filter total,
276    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
277    fn data_models(
278        &mut self,
279        view: &DataModelListView,
280        meta: &MetaContext,
281    ) -> Result<(), Diagnostic>;
282
283    /// Render a `dsp vre data-model describe` result (a single data-model).
284    ///
285    /// `detail` is passed directly — no view wrapper, since there is no aggregate
286    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
287    /// auth/server disclosure per ADR-0007.
288    fn data_model_describe(
289        &mut self,
290        detail: &DataModelDetail,
291        meta: &MetaContext,
292    ) -> Result<(), Diagnostic>;
293
294    /// Render a `dsp vre resource-type list` result (possibly empty).
295    ///
296    /// `view` carries the items (post-filter, sorted), the pre-filter total, the
297    /// filter string, and the parent data-model name. `meta` carries auth/server
298    /// disclosure (ADR-0007).
299    fn resource_types(
300        &mut self,
301        view: &ResourceTypeListView,
302        meta: &MetaContext,
303    ) -> Result<(), Diagnostic>;
304
305    /// Render a `dsp vre resource-type describe` result (a single resource-type).
306    ///
307    /// `detail` is passed directly — no view wrapper, since there is no aggregate
308    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
309    /// auth/server disclosure per ADR-0007. Built-in field filtering is applied by
310    /// the action (via `--include-builtins`) before this method is called — the
311    /// renderer receives only the fields it should render.
312    fn resource_type_describe(
313        &mut self,
314        detail: &ResourceTypeDetail,
315        meta: &MetaContext,
316    ) -> Result<(), Diagnostic>;
317
318    /// Render a `dsp vre data-model structure` result (a single data-model's relations).
319    ///
320    /// `structure` is passed directly — no view wrapper (describe-shaped, like
321    /// `data_model_describe`). Built-in relation filtering via `--include-builtins`
322    /// is applied by the action before this method is called; the renderer receives
323    /// only the relations it should render. `meta` carries auth/server disclosure
324    /// per ADR-0007.
325    fn data_model_structure(
326        &mut self,
327        structure: &DataModelStructure,
328        meta: &MetaContext,
329    ) -> Result<(), Diagnostic>;
330
331    /// Render a `dsp vre resource list` result (possibly empty).
332    ///
333    /// `view` carries the items (post-filter), the pre-filter total, the filter
334    /// string, the resource type name, and the pagination state. `meta` carries
335    /// auth/server disclosure (ADR-0007) including the always-present
336    /// `filter_warning` for instance-side commands (D3).
337    fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic>;
338
339    /// Render a `dsp vre resource describe` result (a single resource's envelope).
340    ///
341    /// `detail` is passed directly — no view wrapper, since there is no aggregate
342    /// context for a single-object describe. `meta` carries auth/server disclosure
343    /// per ADR-0007 including the always-present `filter_warning` for instance-side
344    /// commands (D3).
345    fn resource_describe(
346        &mut self,
347        detail: &ResourceDetail,
348        meta: &MetaContext,
349    ) -> Result<(), Diagnostic>;
350}