Module: SmilyCli::Formatters
- Defined in:
- lib/smily_cli/formatters.rb
Overview
Rendering of Result records into the output formats the CLI supports. Formatters operate on plain record hashes (string keys) and return a String; the command layer decides where to print it. Table output is human-first (aligned columns, optional color, vertical layout for single records); the rest are pipe-friendly and lossless.
Constant Summary collapse
- MAX_COL_WIDTH =
Maximum width of a single table column before truncation.
48- MAX_TABLE_COLUMNS =
Default cap on the number of columns shown in a multi-row table.
8
Class Method Summary collapse
- .column_widths(records, columns) ⇒ Object private
-
.csv(records, fields: nil) ⇒ String
CSV with a header row; nested values become compact JSON.
- .deep_stringify(value) ⇒ Object private
- .default_columns(records) ⇒ Object private
- .grid_table(records, fields:, meta: nil) ⇒ Object private
- .hidden_field_note(records) ⇒ Object private
-
.json(payload) ⇒ String
Pretty JSON (object for single, array for collections).
-
.ndjson(records) ⇒ String
One compact JSON object per line.
- .numeric_columns(records, columns) ⇒ Object private
- .pad(text, width, right: false) ⇒ Object private
-
.render(result, format:, fields: nil) ⇒ String
Render a result in the requested format.
-
.rowify(records) ⇒ Object
private
Coerce records so table/CSV rendering never assumes a Hash: a non-hash record (a scalar or array an arbitrary MCP tool might return) becomes a single
valuecolumn. -
.scalarize(value, limit: nil) ⇒ Object
private
Render a value as a single-line string suitable for a cell/CSV field.
-
.table(result, fields: nil) ⇒ String
Render a table: a vertical key/value layout for a single record, or an aligned grid for a collection.
- .table_footer(result, records) ⇒ Object private
- .truncate(text, width) ⇒ Object private
- .union_columns(records) ⇒ Object private
- .vertical_table(record, fields: nil) ⇒ Object private
- .yaml(payload) ⇒ String
Class Method Details
.column_widths(records, columns) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
168 169 170 171 172 173 174 |
# File 'lib/smily_cli/formatters.rb', line 168 def column_widths(records, columns) columns.map do |col| cells = records.map { |r| scalarize(r[col]).length } widest = [col.to_s.length, *cells].max [widest, MAX_COL_WIDTH].min end end |
.csv(records, fields: nil) ⇒ String
Returns CSV with a header row; nested values become compact JSON.
58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/smily_cli/formatters.rb', line 58 def csv(records, fields: nil) return "" if records.empty? rows = rowify(records) columns = fields || union_columns(rows) CSV.generate do |out| out << columns rows.each do |record| out << columns.map { |c| scalarize(record[c]) } end end end |
.deep_stringify(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
214 215 216 217 218 219 220 |
# File 'lib/smily_cli/formatters.rb', line 214 def deep_stringify(value) case value when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify(v) } when Array then value.map { |v| deep_stringify(v) } else value end end |
.default_columns(records) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
153 154 155 156 157 158 |
# File 'lib/smily_cli/formatters.rb', line 153 def default_columns(records) cols = union_columns(records) preferred = %w[id] ordered = preferred.select { |c| cols.include?(c) } + (cols - preferred) ordered.first(MAX_TABLE_COLUMNS) end |
.grid_table(records, fields:, meta: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/smily_cli/formatters.rb', line 111 def grid_table(records, fields:, meta: nil) columns = fields || default_columns(records) widths = column_widths(records, columns) numeric = numeric_columns(records, columns) header = columns.map.with_index do |col, i| Color.bold(pad(col.to_s, widths[i], right: numeric[col])) end.join(" ") rule = Color.dim(columns.map.with_index { |_, i| "-" * widths[i] }.join(" ")) rows = records.map do |record| columns.map.with_index do |col, i| pad(scalarize(record[col]), widths[i], right: numeric[col]) end.join(" ") end [header, rule, *rows, ].compact.join("\n") end |
.hidden_field_note(records) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
142 143 144 145 146 147 148 149 150 |
# File 'lib/smily_cli/formatters.rb', line 142 def hidden_field_note(records) return nil if records.empty? total = union_columns(records).length shown = default_columns(records).length return nil if total <= shown "#{total - shown} more fields — use --fields or -o json" end |
.json(payload) ⇒ String
Returns pretty JSON (object for single, array for collections).
43 44 45 |
# File 'lib/smily_cli/formatters.rb', line 43 def json(payload) JSON.pretty_generate(payload.nil? ? [] : payload) end |
.ndjson(records) ⇒ String
Returns one compact JSON object per line.
53 54 55 |
# File 'lib/smily_cli/formatters.rb', line 53 def ndjson(records) records.map { |r| JSON.generate(r) }.join("\n") end |
.numeric_columns(records, columns) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
177 178 179 180 181 182 |
# File 'lib/smily_cli/formatters.rb', line 177 def numeric_columns(records, columns) columns.each_with_object({}) do |col, acc| values = records.map { |r| r[col] }.compact acc[col] = !values.empty? && values.all?(Numeric) end end |
.pad(text, width, right: false) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
185 186 187 188 |
# File 'lib/smily_cli/formatters.rb', line 185 def pad(text, width, right: false) s = truncate(text.to_s, width) right ? s.rjust(width) : s.ljust(width) end |
.render(result, format:, fields: nil) ⇒ String
Render a result in the requested format.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/smily_cli/formatters.rb', line 27 def render(result, format:, fields: nil) records = result.records payload = result.single? ? records.first : records case format when "json" then json(payload) when "yaml" then yaml(payload) when "ndjson", "jsonl" then ndjson(records) when "csv" then csv(records, fields: fields) when "table" then table(result, fields: fields) else raise UsageError, "Unknown output format #{format.inspect}." end end |
.rowify(records) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Coerce records so table/CSV rendering never assumes a Hash: a non-hash
record (a scalar or array an arbitrary MCP tool might return) becomes a
single value column.
95 96 97 |
# File 'lib/smily_cli/formatters.rb', line 95 def rowify(records) records.map { |record| record.is_a?(Hash) ? record : { "value" => record } } end |
.scalarize(value, limit: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Render a value as a single-line string suitable for a cell/CSV field. Scalars pass through; Hash/Array become compact JSON.
202 203 204 205 206 207 208 209 210 211 |
# File 'lib/smily_cli/formatters.rb', line 202 def scalarize(value, limit: nil) s = case value when nil then "" when String then value when Hash, Array then JSON.generate(value) else value.to_s end s = s.gsub(/\s+/, " ").strip limit ? truncate(s, limit) : s end |
.table(result, fields: nil) ⇒ String
Render a table: a vertical key/value layout for a single record, or an aligned grid for a collection.
77 78 79 80 81 82 83 84 85 86 |
# File 'lib/smily_cli/formatters.rb', line 77 def table(result, fields: nil) records = rowify(result.records) return Color.dim("No records found.") if records.empty? if result.single? vertical_table(records.first, fields: fields) else grid_table(records, fields: fields, meta: (result, records)) end end |
.table_footer(result, records) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
131 132 133 134 135 136 137 138 139 |
# File 'lib/smily_cli/formatters.rb', line 131 def (result, records) count = records.length parts = [] parts << "#{count} #{count == 1 ? 'record' : 'records'}" parts << "#{result.total_pages} pages total" if result.total_pages hidden = hidden_field_note(records) parts << hidden if hidden Color.dim("(#{parts.join(', ')})") end |
.truncate(text, width) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
191 192 193 194 195 196 |
# File 'lib/smily_cli/formatters.rb', line 191 def truncate(text, width) return text if text.length <= width return text[0, width] if width <= 1 "#{text[0, width - 1]}…" end |
.union_columns(records) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
161 162 163 164 165 |
# File 'lib/smily_cli/formatters.rb', line 161 def union_columns(records) records.each_with_object([]) do |record, acc| record.each_key { |k| acc << k.to_s unless acc.include?(k.to_s) } end end |
.vertical_table(record, fields: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
100 101 102 103 104 105 106 107 108 |
# File 'lib/smily_cli/formatters.rb', line 100 def vertical_table(record, fields: nil) columns = fields || record.keys width = columns.map { |c| c.to_s.length }.max || 0 lines = columns.map do |col| label = Color.bold(col.to_s.ljust(width)) "#{label} #{scalarize(record[col], limit: 200)}" end lines.join("\n") end |
.yaml(payload) ⇒ String
48 49 50 |
# File 'lib/smily_cli/formatters.rb', line 48 def yaml(payload) YAML.dump(deep_stringify(payload.nil? ? [] : payload)) end |