reifydb_core/value/frame/
render.rs1use std::fmt::{self, Write};
5
6use reifydb_value::{
7 reifydb_assertions,
8 util::unicode::UnicodeWidthStr,
9 value::frame::{column::FrameColumn, frame::Frame},
10};
11
12pub struct FrameRenderer;
13
14impl FrameRenderer {
15 pub fn render_full(frame: &Frame) -> Result<String, fmt::Error> {
16 let mut output = String::new();
17 Self::render_full_to(frame, &mut output)?;
18 Ok(output)
19 }
20
21 pub fn render_without_row_numbers(frame: &Frame) -> Result<String, fmt::Error> {
22 let mut output = String::new();
23 Self::render_without_row_numbers_to(frame, &mut output)?;
24 Ok(output)
25 }
26
27 pub fn render_full_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
28 Self::render_internal(frame, f, true)
29 }
30
31 pub fn render_without_row_numbers_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
32 Self::render_internal(frame, f, false)
33 }
34
35 fn render_internal(frame: &Frame, f: &mut dyn Write, include_row_numbers: bool) -> fmt::Result {
36 let row_count = frame.first().map_or(0, |c| c.data.len());
37 let has_row_numbers = include_row_numbers && !frame.row_numbers.is_empty();
38 let has_created_at = !frame.created_at.is_empty();
39 let has_updated_at = !frame.updated_at.is_empty();
40 let column_order = Self::get_column_display_order(frame);
41
42 let col_widths = Self::compute_column_widths(
43 frame,
44 &column_order,
45 row_count,
46 has_row_numbers,
47 has_created_at,
48 has_updated_at,
49 );
50 let sep = Self::separator_line(&col_widths);
51
52 writeln!(f, "{}", sep)?;
53 Self::emit_header(
54 frame,
55 f,
56 &column_order,
57 &col_widths,
58 has_row_numbers,
59 has_created_at,
60 has_updated_at,
61 )?;
62 writeln!(f, "{}", sep)?;
63 Self::emit_data_rows(
64 frame,
65 f,
66 &column_order,
67 &col_widths,
68 row_count,
69 has_row_numbers,
70 has_created_at,
71 has_updated_at,
72 )?;
73 writeln!(f, "{}", sep)
74 }
75
76 #[inline]
77 #[allow(clippy::too_many_arguments)]
78 fn compute_column_widths(
79 frame: &Frame,
80 column_order: &[usize],
81 row_count: usize,
82 has_row_numbers: bool,
83 has_created_at: bool,
84 has_updated_at: bool,
85 ) -> Vec<usize> {
86 let col_count = frame.len()
87 + if has_row_numbers {
88 1
89 } else {
90 0
91 } + if has_created_at {
92 1
93 } else {
94 0
95 } + if has_updated_at {
96 1
97 } else {
98 0
99 };
100
101 let mut col_widths = vec![0; col_count];
102
103 let mut sys_col_idx = 0;
104 if has_row_numbers {
105 col_widths[sys_col_idx] = Self::display_width("#rownum");
106 for row_num in &frame.row_numbers {
107 col_widths[sys_col_idx] =
108 col_widths[sys_col_idx].max(Self::display_width(&row_num.to_string()));
109 }
110 sys_col_idx += 1;
111 }
112 if has_created_at {
113 col_widths[sys_col_idx] = Self::display_width("#created_at");
114 for ts in &frame.created_at {
115 col_widths[sys_col_idx] =
116 col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
117 }
118 sys_col_idx += 1;
119 }
120 if has_updated_at {
121 col_widths[sys_col_idx] = Self::display_width("#updated_at");
122 for ts in &frame.updated_at {
123 col_widths[sys_col_idx] =
124 col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
125 }
126 sys_col_idx += 1;
127 }
128 let row_num_col_idx = sys_col_idx;
129
130 for (display_idx, &col_idx) in column_order.iter().enumerate() {
131 let col = &frame[col_idx];
132 let display_name = Self::escape_control_chars(&col.name);
133 col_widths[row_num_col_idx + display_idx] = Self::display_width(&display_name);
134 }
135
136 for row_numberx in 0..row_count {
137 for (display_idx, &col_idx) in column_order.iter().enumerate() {
138 let col = &frame[col_idx];
139 let s = Self::extract_string_value(col, row_numberx);
140 col_widths[row_num_col_idx + display_idx] =
141 col_widths[row_num_col_idx + display_idx].max(Self::display_width(&s));
142 }
143 }
144
145 for w in &mut col_widths {
146 *w += 2;
147 }
148
149 col_widths
150 }
151
152 #[inline]
153 fn separator_line(col_widths: &[usize]) -> String {
154 format!("+{}+", col_widths.iter().map(|w| "-".repeat(*w + 2)).collect::<Vec<_>>().join("+"))
155 }
156
157 #[inline]
158 #[allow(clippy::too_many_arguments)]
159 fn emit_header(
160 frame: &Frame,
161 f: &mut dyn Write,
162 column_order: &[usize],
163 col_widths: &[usize],
164 has_row_numbers: bool,
165 has_created_at: bool,
166 has_updated_at: bool,
167 ) -> fmt::Result {
168 let mut header = Vec::new();
169
170 let mut sys_idx = 0;
171 if has_row_numbers {
172 header.push(Self::format_cell(col_widths[sys_idx], "#rownum"));
173 sys_idx += 1;
174 }
175 if has_created_at {
176 header.push(Self::format_cell(col_widths[sys_idx], "#created_at"));
177 sys_idx += 1;
178 }
179 if has_updated_at {
180 header.push(Self::format_cell(col_widths[sys_idx], "#updated_at"));
181 sys_idx += 1;
182 }
183 let row_num_col_idx = sys_idx;
184
185 reifydb_assertions! {
186 let needed = row_num_col_idx + column_order.len();
187 assert!(
188 col_widths.len() >= needed,
189 "header system-column count diverged from compute_column_widths, so a data \
190 column would index col_widths out of bounds and panic mid-render \
191 (system cols={row_num_col_idx}, data cols={}, col_widths.len()={})",
192 column_order.len(),
193 col_widths.len()
194 );
195 }
196
197 for (display_idx, &col_idx) in column_order.iter().enumerate() {
198 let col = &frame[col_idx];
199 let name = Self::escape_control_chars(&col.name);
200 header.push(Self::format_cell(col_widths[row_num_col_idx + display_idx], &name));
201 }
202
203 writeln!(f, "|{}|", header.join("|"))
204 }
205
206 #[inline]
207 #[allow(clippy::too_many_arguments)]
208 fn emit_data_rows(
209 frame: &Frame,
210 f: &mut dyn Write,
211 column_order: &[usize],
212 col_widths: &[usize],
213 row_count: usize,
214 has_row_numbers: bool,
215 has_created_at: bool,
216 has_updated_at: bool,
217 ) -> fmt::Result {
218 for row_numberx in 0..row_count {
219 let mut row = Vec::new();
220
221 let mut sys_idx = 0;
222 if has_row_numbers {
223 let s = frame.row_numbers[row_numberx].to_string();
224 row.push(Self::format_cell(col_widths[sys_idx], &s));
225 sys_idx += 1;
226 }
227 if has_created_at {
228 let s = frame.created_at[row_numberx].to_string();
229 row.push(Self::format_cell(col_widths[sys_idx], &s));
230 sys_idx += 1;
231 }
232 if has_updated_at {
233 let s = frame.updated_at[row_numberx].to_string();
234 row.push(Self::format_cell(col_widths[sys_idx], &s));
235 sys_idx += 1;
236 }
237 let row_num_col_idx = sys_idx;
238
239 reifydb_assertions! {
240 let needed = row_num_col_idx + column_order.len();
241 assert!(
242 col_widths.len() >= needed,
243 "data-row system-column count diverged from compute_column_widths, so a data \
244 column would index col_widths out of bounds and panic mid-render \
245 (system cols={row_num_col_idx}, data cols={}, col_widths.len()={})",
246 column_order.len(),
247 col_widths.len()
248 );
249 }
250
251 for (display_idx, &col_idx) in column_order.iter().enumerate() {
252 let col = &frame[col_idx];
253 let s = Self::extract_string_value(col, row_numberx);
254 row.push(Self::format_cell(col_widths[row_num_col_idx + display_idx], &s));
255 }
256
257 writeln!(f, "|{}|", row.join("|"))?;
258 }
259
260 Ok(())
261 }
262
263 fn format_cell(width: usize, content: &str) -> String {
264 let pad = width - Self::display_width(content);
265 let l = pad / 2;
266 let r = pad - l;
267 format!(" {:left$}{}{:right$} ", "", content, "", left = l, right = r)
268 }
269
270 fn display_width(s: &str) -> usize {
271 if s.contains('\n') {
272 s.lines().map(|line| line.width()).max().unwrap_or(0)
273 } else {
274 s.width()
275 }
276 }
277
278 fn escape_control_chars(s: &str) -> String {
279 s.replace('\n', "\\n").replace('\t', "\\t")
280 }
281
282 fn get_column_display_order(frame: &Frame) -> Vec<usize> {
283 (0..frame.len()).collect()
284 }
285
286 fn extract_string_value(col: &FrameColumn, row_numberx: usize) -> String {
287 let s = col.data.as_string(row_numberx);
288 Self::escape_control_chars(&s)
289 }
290}