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;
25pub(crate) mod vocabulary;
26
27pub use auth::{AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome};
28pub use dump::{DumpDeleteOutcome, DumpEvent, DumpOutcome};
29pub use format::Format;
30pub use progress::{HumanProgress, JsonProgress, ProgressReporter};
31// Re-exported for plan 020 steps 2–5: renderer methods, engine error hints,
32// and CLI `after_help` drift-guard tests.
33pub(crate) use table::{
34    AUTH_LOGIN_COLUMNS, AUTH_LOGOUT_COLUMNS, DATA_MODEL_DESCRIBE_COLUMNS,
35    DATA_MODEL_STRUCTURE_COLUMNS, DATA_MODELS_COLUMNS, PROJECT_DUMP_COLUMNS,
36    PROJECT_DUMP_DELETED_COLUMNS, PROJECTS_COLUMNS, QuoteMode, RESOURCE_DESCRIBE_COLUMNS,
37    RESOURCE_DESCRIBE_VALUES_COLUMNS, RESOURCE_DESCRIBE_VALUES_DEFAULT_COLUMNS,
38    RESOURCE_LIST_COLUMNS, RESOURCE_TYPE_DESCRIBE_COLUMNS, RESOURCE_TYPE_DESCRIBE_DEFAULT_COLUMNS,
39    RESOURCE_TYPES_COLUMNS, RESOURCE_TYPES_DEFAULT_COLUMNS, TableSpec, VOCABULARIES_COLUMNS,
40    VOCABULARIES_COUNTED_DEFAULT_COLUMNS, VOCABULARIES_DEFAULT_COLUMNS,
41    VOCABULARY_DESCRIBE_COLUMNS, VOCABULARY_DESCRIBE_DEFAULT_COLUMNS, render_table,
42};
43// TableOptions and HeaderMode are public: integration tests in `tests/` construct
44// renderers with specific options via `with_options`. The column-set consts are
45// pub(crate) only (used in renderer bodies and the engine error hints, not the
46// test API).
47//
48// `with_options` is `pub` (not `pub(crate)`) because integration tests are a
49// separate crate and need to call it to exercise the new --columns and header
50// flags end-to-end; bypassing FormatArgs::table_options validation via
51// with_options is an internal/test-only concern that does not affect the
52// production dispatch path.
53pub use table::{HeaderMode, TableOptions};
54
55use crate::diagnostic::Diagnostic;
56use crate::model::{
57    DataModel, DataModelDetail, DataModelStructure, Project, ProjectDetail, ResourceDetail,
58    ResourceSummary, ResourceType, ResourceTypeDetail, Vocabulary, VocabularyDetail,
59};
60
61/// The data a renderer needs to render a `project list` result.
62///
63/// `total` is the pre-filter count; `filter` is the applied substring (if any),
64/// used only by prose for the "(m of n matching "…")" count line.
65///
66/// Owns its data (no borrow/lifetime): it is a per-call view, never stored, and
67/// the action builds it by moving the already-sorted `Vec<Project>` in. Owning
68/// avoids a `'a` parameter leaking into the `Renderer::projects` signature (and
69/// the latent lifetime-elision trap a future caching renderer would hit). The
70/// clone cost is nil — the vec is moved, not copied.
71#[derive(Debug)]
72pub struct ProjectListView {
73    pub items: Vec<Project>,
74    /// Pre-filter total count (before `--filter` was applied). Feeds the
75    /// "(m of total matching …)" prose count line.
76    pub total: usize,
77    /// The `--filter` substring the user supplied, if any.
78    pub filter: Option<String>,
79}
80
81/// The data a renderer needs to render a `data-model list` result.
82///
83/// `total` is the pre-filter count (it includes any built-ins the action
84/// appended); `filter` the applied substring (if any). Whether built-ins are
85/// present is read off the items themselves (`is_builtin`), so it is not carried
86/// as a separate field.
87#[derive(Debug, Clone)]
88pub struct DataModelListView {
89    pub items: Vec<DataModel>,
90    /// Pre-filter total count (before `--filter` was applied). Feeds the
91    /// "(m of total matching …)" prose count line.
92    pub total: usize,
93    /// The `--filter` substring the user supplied, if any.
94    pub filter: Option<String>,
95}
96
97/// The data a renderer needs to render a `resource-type list` result.
98///
99/// `items` is the post-filter, sorted list; `total` is the pre-filter count
100/// (after any built-ins were appended, before `--filter` was applied); `filter`
101/// is the applied substring (if any). Whether built-ins are present is read off
102/// the items themselves (`is_builtin`), so it is not carried as a separate field.
103///
104/// `data_model` carries the resolved parent data-model's **name** for the prose
105/// header (`resource-types in <data_model> on <server>`). This is a deliberate
106/// extension beyond `ProjectListView`/`DataModelListView` (which carry no parent):
107/// resource-type list is sub-scoped to one data-model, and the name must appear
108/// even when `items` is empty (so it cannot be derived from `items[0].iri`).
109/// Threading it through `MetaContext` was rejected — that struct is auth/server
110/// disclosure only (ADR-0007), so overloading it is a worse coupling than this
111/// explicit field. Tabular and JSON renderers ignore `data_model`.
112///
113/// `Clone` is required because the test `RecordingRenderer` stores the view in
114/// an `Option<ResourceTypeListView>`.
115#[derive(Debug, Clone)]
116pub struct ResourceTypeListView {
117    pub items: Vec<ResourceType>,
118    /// Pre-filter total count (after built-ins were appended, before `--filter`
119    /// was applied). Feeds the "(m of total matching …)" prose count line.
120    pub total: usize,
121    /// The `--filter` substring the user supplied, if any.
122    pub filter: Option<String>,
123    /// The resolved parent data-model's NAME, for the prose header. Tabular/json
124    /// ignore it. See struct-level doc for why this field exists.
125    pub data_model: String,
126}
127
128/// Pagination state for a `resource list` render call (D5 of the plan).
129///
130/// Models two mutually exclusive modes as an enum rather than two loose
131/// `Option<u32>` fields that admit invalid combinations (e.g. both set).
132/// The json renderer matches on this to build `_meta` pagination keys;
133/// prose reads `may_have_more` (SinglePage only) for the "use --all" hint.
134///
135/// `AllPages` hard-codes `may_have_more_results: false` because the `--all`
136/// loop only exits when the server reports `false` — the variant carries no
137/// flag because it is guaranteed.
138#[derive(Debug, Clone)]
139pub enum ResourceListPagination {
140    /// A single page was fetched (default or `--page N`).
141    SinglePage {
142        /// The page number that was fetched (zero-based).
143        page: u32,
144        /// Whether the server reported more pages after this one.
145        may_have_more: bool,
146    },
147    /// All pages were fetched (`--all`).
148    AllPages {
149        /// Total number of pages fetched.
150        pages_fetched: u32,
151    },
152}
153
154/// The data a renderer needs to render a `resource list` result.
155///
156/// `items` is the post-filter list; `total` is the pre-filter count (capture
157/// BEFORE applying `--filter`, mirroring `project list`); `filter` is the
158/// applied substring (if any). `resource_type` carries the resolved class name
159/// for the prose header. `pagination` carries the D5 mode struct (single page
160/// vs. all pages).
161///
162/// `Clone` is required because the test `RecordingRenderer` stores the view in
163/// an `Option<ResourceListView>`.
164#[derive(Debug, Clone)]
165pub struct ResourceListView {
166    /// Post-filter, sorted resource summaries.
167    pub items: Vec<ResourceSummary>,
168    /// Pre-filter total count. Feeds the "(m of total matching "…")" prose line.
169    pub total: usize,
170    /// The `--filter` substring the user supplied, if any.
171    pub filter: Option<String>,
172    /// Local name of the resource type, for the prose header.
173    pub resource_type: String,
174    /// Pagination state (single page vs. all-pages drain).
175    pub pagination: ResourceListPagination,
176}
177
178/// The data a renderer needs to render a `vocabulary list` result.
179#[derive(Debug, Clone)]
180pub struct VocabularyListView {
181    pub items: Vec<Vocabulary>,
182    /// Pre-filter total count (before `--filter` was applied). Feeds the
183    /// "(m of total matching …)" prose count line.
184    pub total: usize,
185    /// The `--filter` substring the user supplied, if any.
186    pub filter: Option<String>,
187    /// Whether `--count` was passed (drives which tabular default-column set
188    /// applies and the prose "· N nodes · M levels" suffix per item). NOT the
189    /// same test as "any item carries a count" — an item can carry `None`
190    /// after a failed per-tree fetch even when `--count` WAS passed; `counted`
191    /// records the flag, not the outcome.
192    pub counted: bool,
193}
194
195/// Auth and server context attached to every rendered response.
196/// See ADR-0007.
197#[derive(Debug, Clone)]
198pub struct MetaContext {
199    pub server_label: String,
200    pub auth_state: String,
201    /// ADR-0007 silent-filter disclosure for instance-side reads.
202    ///
203    /// Set to `Some(message)` by instance-side commands (`resource list`,
204    /// `resource describe`) to disclose that results may be filtered by the
205    /// caller's authentication state (anonymous → only public resources visible;
206    /// authenticated → bounded by permissions). `None` for schema-side commands
207    /// (`project list/describe`, `data-model list/describe`, `resource-type
208    /// list/describe`, `data-model structure`) that are not affected by caller
209    /// identity.
210    pub filter_warning: Option<String>,
211    /// Schema-side `--count` disclosure note (`resource-type list`/`describe`).
212    ///
213    /// Set to `Some(message)` by the action layer when `--count` was passed,
214    /// disclosing that the v3 `resourcesPerOntology` counts are NOT
215    /// permission-filtered (unlike `resource list`'s `filter_warning`, which IS
216    /// permission-filtered) and exclude deleted resources. `None` when `--count`
217    /// was not used, and always `None` for every command other than
218    /// resource-type list/describe. Deliberately a DISTINCT field from
219    /// `filter_warning` — a different semantic contract, not reused (see plan
220    /// docs/design/plans/030-resource-type-count/implementation-plan.md).
221    pub count_caveat: Option<String>,
222    /// `dsp vre vocabulary list --count` cost-disclosure note (plan 034).
223    ///
224    /// Set to `Some(message)` by the action layer when `--count` was passed on
225    /// `vocabulary list`, disclosing that `--count` costs one extra tree fetch
226    /// PER vocabulary (sequential, unthrottled). Deliberately a DISTINCT field
227    /// from `count_caveat` — `count_caveat`'s message is about permission-filtering
228    /// accuracy, which does not apply here: vocabularies are public and their
229    /// counts are exact. `None` when `--count` was not used, and always `None`
230    /// for every command other than `vocabulary list`.
231    pub count_cost: Option<String>,
232}
233
234/// The `Renderer` trait. Methods grow as new noun-groups land.
235///
236/// All methods return `Result<(), Diagnostic>`. I/O errors in renderer impls
237/// are converted via `impl From<std::io::Error> for Diagnostic`, which means
238/// `writeln!(self.out, "...")?;` Just Works against this return type.
239pub trait Renderer {
240    /// Render a diagnostic in the appropriate shape for this format.
241    fn diagnostic(&mut self, diag: &Diagnostic, meta: &MetaContext) -> Result<(), Diagnostic>;
242
243    /// Render a successful `dsp auth login` outcome.
244    fn auth_login(
245        &mut self,
246        outcome: &AuthLoginOutcome,
247        meta: &MetaContext,
248    ) -> Result<(), Diagnostic>;
249
250    /// Render a `dsp auth status` outcome (logged-in or not-logged-in).
251    fn auth_status(
252        &mut self,
253        outcome: &AuthStatusOutcome,
254        meta: &MetaContext,
255    ) -> Result<(), Diagnostic>;
256
257    /// Render a `dsp auth logout` outcome.
258    fn auth_logout(
259        &mut self,
260        outcome: &AuthLogoutOutcome,
261        meta: &MetaContext,
262    ) -> Result<(), Diagnostic>;
263
264    /// Render a successful `dsp auth set-token` outcome.
265    fn auth_set_token(
266        &mut self,
267        outcome: &AuthSetTokenOutcome,
268        meta: &MetaContext,
269    ) -> Result<(), Diagnostic>;
270
271    /// Render a `dsp vre project dump` outcome.
272    fn project_dump(&mut self, outcome: &DumpOutcome, meta: &MetaContext)
273    -> Result<(), Diagnostic>;
274
275    /// Render a `dsp vre project dump --delete` outcome.
276    ///
277    /// `outcome.deleted = false` means no completed/failed dump existed and a
278    /// probe created an in-progress dump — NOT a delete failure (failures are
279    /// `Err(Diagnostic)`).
280    fn project_dump_deleted(
281        &mut self,
282        outcome: &DumpDeleteOutcome,
283        meta: &MetaContext,
284    ) -> Result<(), Diagnostic>;
285
286    /// Render a `dsp vre project list` result (possibly empty).
287    ///
288    /// `view` carries the items (post-filter, sorted), the pre-filter total,
289    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
290    fn projects(&mut self, view: &ProjectListView, meta: &MetaContext) -> Result<(), Diagnostic>;
291
292    /// Render a `dsp vre project describe` result (a single project).
293    ///
294    /// `project` is passed directly — no view wrapper, since there is no
295    /// aggregate context (no `total`/`filter`) for a single-object describe.
296    /// `meta` carries auth/server disclosure per ADR-0007.
297    fn project_describe(
298        &mut self,
299        project: &ProjectDetail,
300        meta: &MetaContext,
301    ) -> Result<(), Diagnostic>;
302
303    /// Render a `dsp vre data-model list` result (possibly empty).
304    ///
305    /// `view` carries the items (post-filter, sorted), the pre-filter total,
306    /// and the filter string. `meta` carries auth/server disclosure (ADR-0007).
307    fn data_models(
308        &mut self,
309        view: &DataModelListView,
310        meta: &MetaContext,
311    ) -> Result<(), Diagnostic>;
312
313    /// Render a `dsp vre data-model describe` result (a single data-model).
314    ///
315    /// `detail` is passed directly — no view wrapper, since there is no aggregate
316    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
317    /// auth/server disclosure per ADR-0007.
318    fn data_model_describe(
319        &mut self,
320        detail: &DataModelDetail,
321        meta: &MetaContext,
322    ) -> Result<(), Diagnostic>;
323
324    /// Render a `dsp vre resource-type list` result (possibly empty).
325    ///
326    /// `view` carries the items (post-filter, sorted), the pre-filter total, the
327    /// filter string, and the parent data-model name. `meta` carries auth/server
328    /// disclosure (ADR-0007).
329    fn resource_types(
330        &mut self,
331        view: &ResourceTypeListView,
332        meta: &MetaContext,
333    ) -> Result<(), Diagnostic>;
334
335    /// Render a `dsp vre resource-type describe` result (a single resource-type).
336    ///
337    /// `detail` is passed directly — no view wrapper, since there is no aggregate
338    /// context (no `total`/`filter`) for a single-object describe. `meta` carries
339    /// auth/server disclosure per ADR-0007. Built-in field filtering is applied by
340    /// the action (via `--include-builtins`) before this method is called — the
341    /// renderer receives only the fields it should render.
342    fn resource_type_describe(
343        &mut self,
344        detail: &ResourceTypeDetail,
345        meta: &MetaContext,
346    ) -> Result<(), Diagnostic>;
347
348    /// Render a `dsp vre data-model structure` result (a single data-model's relations).
349    ///
350    /// `structure` is passed directly — no view wrapper (describe-shaped, like
351    /// `data_model_describe`). Built-in relation filtering via `--include-builtins`
352    /// is applied by the action before this method is called; the renderer receives
353    /// only the relations it should render. `meta` carries auth/server disclosure
354    /// per ADR-0007.
355    fn data_model_structure(
356        &mut self,
357        structure: &DataModelStructure,
358        meta: &MetaContext,
359    ) -> Result<(), Diagnostic>;
360
361    /// Render a `dsp vre resource list` result (possibly empty).
362    ///
363    /// `view` carries the items (post-filter), the pre-filter total, the filter
364    /// string, the resource type name, and the pagination state. `meta` carries
365    /// auth/server disclosure (ADR-0007) including the always-present
366    /// `filter_warning` for instance-side commands (D3).
367    fn resources(&mut self, view: &ResourceListView, meta: &MetaContext) -> Result<(), Diagnostic>;
368
369    /// Render a `dsp vre resource describe` result (a single resource's envelope).
370    ///
371    /// `detail` is passed directly — no view wrapper, since there is no aggregate
372    /// context for a single-object describe. `meta` carries auth/server disclosure
373    /// per ADR-0007 including the always-present `filter_warning` for instance-side
374    /// commands (D3).
375    fn resource_describe(
376        &mut self,
377        detail: &ResourceDetail,
378        meta: &MetaContext,
379    ) -> Result<(), Diagnostic>;
380
381    /// Render a `dsp vre vocabulary list` result (possibly empty).
382    ///
383    /// `view` carries the items (post-filter, sorted by name), the pre-filter
384    /// total, the filter string, and whether `--count` was requested. `meta`
385    /// carries auth/server disclosure (ADR-0007) plus the `--count` cost
386    /// disclosure (`MetaContext.count_cost`, plan 034).
387    fn vocabularies(
388        &mut self,
389        view: &VocabularyListView,
390        meta: &MetaContext,
391    ) -> Result<(), Diagnostic>;
392
393    /// Render a `dsp vre vocabulary describe` result (a single vocabulary's tree).
394    ///
395    /// `detail` is passed directly — no view wrapper, since there is no
396    /// aggregate context for a single-object describe.
397    ///
398    /// **This method deliberately INVERTS the established describe convention.**
399    /// Other describe methods document that "built-in field filtering is
400    /// applied by the action ... before this method is called — the renderer
401    /// receives only the fields it should render" (see `resource_type_describe`
402    /// above). This one is the opposite: it receives the WHOLE tree
403    /// (`detail.tree`, never pruned) and filters to `detail.subtree_of`'s branch
404    /// itself. That is required, not sloppy — pruning before the renderer would
405    /// delete the ancestor chain that the absolute `number` and `path` columns
406    /// are derived from. Do not "fix" this by narrowing the tree in the action
407    /// layer.
408    fn vocabulary_describe(
409        &mut self,
410        detail: &VocabularyDetail,
411        meta: &MetaContext,
412    ) -> Result<(), Diagnostic>;
413}