Skip to main content

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}