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