156 lines
4.8 KiB
Rust
156 lines
4.8 KiB
Rust
//! Column configuration for table mode. Defines which fields
|
|
//! to display as columns and how to label them.
|
|
|
|
use crate::item::resolve_field_path;
|
|
use serde_json::Value;
|
|
|
|
/// A single column definition: which field to extract and
|
|
/// what to call it in the header.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnDef {
|
|
/// Dotted path into the item's JSON value (e.g. "meta.res").
|
|
pub field: String,
|
|
/// Display name for the column header. Defaults to the
|
|
/// field path if no alias is given.
|
|
pub display_name: String,
|
|
}
|
|
|
|
/// Column layout for table mode. Controls which fields are
|
|
/// shown and in what order.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnConfig {
|
|
pub columns: Vec<ColumnDef>,
|
|
}
|
|
|
|
impl ColumnConfig {
|
|
/// Parse a column spec string like "label,meta.res:Resolution,meta.size".
|
|
/// Columns are comma-separated. Each column can have an alias
|
|
/// after the first colon: "field:Alias".
|
|
pub fn parse(spec: &str) -> Self {
|
|
let columns = spec
|
|
.split(',')
|
|
.filter(|s| !s.is_empty())
|
|
.map(|part| {
|
|
let part = part.trim();
|
|
if let Some((field, alias)) = part.split_once(':') {
|
|
ColumnDef {
|
|
field: field.trim().to_string(),
|
|
display_name: alias.trim().to_string(),
|
|
}
|
|
} else {
|
|
ColumnDef {
|
|
field: part.to_string(),
|
|
display_name: part.to_string(),
|
|
}
|
|
}
|
|
})
|
|
.collect();
|
|
Self { columns }
|
|
}
|
|
|
|
/// Build a ColumnConfig from raw header names. Used when
|
|
/// auto-generating columns from CSV headers.
|
|
pub fn from_headers(headers: &[String]) -> Self {
|
|
let columns = headers
|
|
.iter()
|
|
.map(|h| ColumnDef {
|
|
field: h.clone(),
|
|
display_name: h.clone(),
|
|
})
|
|
.collect();
|
|
Self { columns }
|
|
}
|
|
|
|
/// Resolve each column's value from an item's JSON value.
|
|
/// Missing fields produce an empty string.
|
|
pub fn resolve_values(&self, value: &Value) -> Vec<String> {
|
|
self.columns
|
|
.iter()
|
|
.map(|col| {
|
|
resolve_field_path(value, &col.field)
|
|
.map(value_to_cell)
|
|
.unwrap_or_default()
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Convert a JSON value to a cell string for display.
|
|
fn value_to_cell(v: &Value) -> String {
|
|
match v {
|
|
Value::String(s) => s.clone(),
|
|
Value::Number(n) => n.to_string(),
|
|
Value::Bool(b) => b.to_string(),
|
|
Value::Null => String::new(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn parse_simple_columns() {
|
|
let config = ColumnConfig::parse("label,age,city");
|
|
assert_eq!(config.columns.len(), 3);
|
|
assert_eq!(config.columns[0].field, "label");
|
|
assert_eq!(config.columns[0].display_name, "label");
|
|
assert_eq!(config.columns[2].field, "city");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_alias() {
|
|
let config = ColumnConfig::parse("label,meta.res:Resolution");
|
|
assert_eq!(config.columns.len(), 2);
|
|
assert_eq!(config.columns[1].field, "meta.res");
|
|
assert_eq!(config.columns[1].display_name, "Resolution");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_empty_string() {
|
|
let config = ColumnConfig::parse("");
|
|
assert!(config.columns.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_trailing_comma() {
|
|
let config = ColumnConfig::parse("label,age,");
|
|
assert_eq!(config.columns.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_flat_values() {
|
|
let config = ColumnConfig::parse("label,age");
|
|
let value = json!({"label": "alice", "age": 30});
|
|
let cells = config.resolve_values(&value);
|
|
assert_eq!(cells, vec!["alice", "30"]);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_nested_values() {
|
|
let config = ColumnConfig::parse("label,meta.res");
|
|
let value = json!({"label": "test", "meta": {"res": "1080p"}});
|
|
let cells = config.resolve_values(&value);
|
|
assert_eq!(cells, vec!["test", "1080p"]);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_missing_field() {
|
|
let config = ColumnConfig::parse("label,nope");
|
|
let value = json!({"label": "test"});
|
|
let cells = config.resolve_values(&value);
|
|
assert_eq!(cells, vec!["test", ""]);
|
|
}
|
|
|
|
#[test]
|
|
fn from_headers_builds_identity() {
|
|
let headers = vec!["name".to_string(), "age".to_string()];
|
|
let config = ColumnConfig::from_headers(&headers);
|
|
assert_eq!(config.columns.len(), 2);
|
|
assert_eq!(config.columns[0].field, "name");
|
|
assert_eq!(config.columns[0].display_name, "name");
|
|
}
|
|
}
|