use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde_json::{json, Map, Value as J};
use crate::model::{BaseFile, SortKey, View, ViewInteractive};
use crate::render::{self, column_header, RenderOptions};
use crate::semver;
use crate::value::Value;
const DEFAULT_MAX_ENUM: usize = 40;
pub(crate) struct RowView<'a> {
pub id: usize,
pub cells: &'a BTreeMap<String, Value>,
}
pub fn view_interactive_enabled(base: &BaseFile, view: &View) -> bool {
if let Some(toggle) = &base.docgen_interactive {
if !toggle.enabled() {
return false;
}
}
if let Some(iv) = &view.interactive {
if iv.enabled == Some(false) {
return false;
}
}
true
}
fn type_tag(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "num",
Value::Str(_) => "str",
Value::Date(_) => "date",
Value::Duration(_) => "dur",
Value::List(_) => "list",
Value::Object(_) => "obj",
Value::Link(_) => "link",
}
}
fn project_cell(v: &Value, semver_col: bool) -> J {
let mut m = Map::new();
m.insert("t".into(), json!(type_tag(v)));
m.insert("d".into(), json!(v.display()));
if let Some(n) = v.as_number() {
m.insert("num".into(), json!(n));
}
if let Value::Date(d) = v {
m.insert("epoch".into(), json!(d.epoch_millis()));
}
if semver_col {
m.insert("sv".into(), json!(semver::column_sort_key(v)));
}
if let Value::List(items) = v {
if !items.is_empty() {
let tokens: Vec<String> = items.iter().map(Value::display).collect();
m.insert("f".into(), json!(tokens));
}
}
if v.is_empty() {
m.insert("empty".into(), json!(true));
}
J::Object(m)
}
fn is_title_column(key: &str) -> bool {
matches!(
key,
"file.name" | "file.basename" | "file.file" | "file.path"
)
}
struct ColMeta {
type_: &'static str,
sortable: bool,
filter: &'static str,
}
fn infer_column(
key: &str,
rows: &[RowView],
max_enum: usize,
overrides: Option<&ViewInteractive>,
) -> ColMeta {
let non_empty: Vec<&Value> = rows
.iter()
.filter_map(|r| r.cells.get(key))
.filter(|v| !v.is_empty())
.collect();
let (type_, mut sortable, mut filter) = if is_title_column(key) {
("str", true, "text")
} else if non_empty.is_empty() {
("str", true, "none")
} else {
infer_from_cells(&non_empty, max_enum)
};
if let Some(iv) = overrides {
if let Some(w) = iv.filters.get(key) {
filter = normalize_widget(w).unwrap_or(filter);
}
if let Some(&s) = iv.sortable.get(key) {
sortable = s;
}
}
ColMeta {
type_,
sortable,
filter,
}
}
fn infer_from_cells(non_empty: &[&Value], max_enum: usize) -> (&'static str, bool, &'static str) {
let tags: BTreeSet<&'static str> = non_empty.iter().map(|v| type_tag(v)).collect();
let type_: &'static str = if tags.len() == 1 {
tags.iter().next().copied().unwrap()
} else if non_empty.iter().all(|v| v.as_number().is_some()) {
"num"
} else {
"str"
};
match type_ {
"date" => ("date", true, "date"),
"num" | "dur" => (type_, true, "number"),
"bool" => ("bool", true, "boolean"),
"list" => {
let mut tokens: BTreeSet<String> = BTreeSet::new();
for v in non_empty {
if let Value::List(items) = v {
for item in items {
tokens.insert(item.display());
}
} else {
tokens.insert(v.display());
}
}
let filter = if tokens.len() <= max_enum {
"enum"
} else {
"text"
};
("list", true, filter)
}
"obj" => ("obj", true, "none"),
_ => {
let distinct: BTreeSet<String> = non_empty.iter().map(|v| v.display()).collect();
let filter = if distinct.len() <= max_enum {
"enum"
} else {
"text"
};
(type_, true, filter)
}
}
}
fn normalize_widget(w: &str) -> Option<&'static str> {
match w.trim().to_ascii_lowercase().as_str() {
"none" => Some("none"),
"text" => Some("text"),
"enum" => Some("enum"),
"date" => Some("date"),
"number" => Some("number"),
"boolean" => Some("boolean"),
_ => None,
}
}
pub(crate) fn build_payload(
view: &View,
base: &BaseFile,
columns: &[String],
rows: &[RowView],
_opts: &RenderOptions,
) -> String {
let overrides = view.interactive.as_ref();
let max_enum = overrides
.and_then(|iv| iv.max_enum)
.unwrap_or(DEFAULT_MAX_ENUM);
let cols_json: Vec<J> = columns
.iter()
.map(|key| {
let meta = infer_column(key, rows, max_enum, overrides);
json!({
"key": key,
"header": column_header(key, base),
"type": meta.type_,
"sortable": meta.sortable,
"filter": meta.filter,
})
})
.collect();
let semver_cols: BTreeSet<&String> = columns
.iter()
.filter(|key| {
render::sorts_as_semver(
view,
key,
rows.iter()
.map(|r| r.cells.get(*key).unwrap_or(&Value::Null)),
)
})
.collect();
let rows_json: Vec<J> = rows
.iter()
.map(|r| {
let mut cells = Map::new();
for key in columns {
let v = r.cells.get(key).cloned().unwrap_or(Value::Null);
cells.insert(key.clone(), project_cell(&v, semver_cols.contains(key)));
}
json!({ "id": r.id, "cells": J::Object(cells) })
})
.collect();
let group_by = view
.group_by
.as_ref()
.filter(|_| view.view_type == "table")
.map(|gb| json!({ "col": gb.property(), "desc": gb.descending() }));
let key_json = |keys: &[SortKey]| -> Vec<J> {
keys.iter()
.map(|k| json!({ "col": k.property(), "desc": k.descending() }))
.collect()
};
let ssr_sort_json = key_json(&view.sort);
let sort_keys = overrides
.filter(|iv| !iv.default_sort.is_empty())
.map(|iv| iv.default_sort.as_slice())
.unwrap_or(view.sort.as_slice());
let sort_json: Vec<J> = key_json(sort_keys);
let search = overrides.and_then(|iv| iv.search).unwrap_or(true);
let page_size = overrides
.and_then(|iv| iv.page_size)
.unwrap_or(if rows.len() > 50 { 50 } else { 0 });
let payload = json!({
"v": 1,
"view": {
"type": view.view_type,
"name": view.name,
"groupBy": group_by,
"sort": ssr_sort_json,
},
"columns": cols_json,
"rows": rows_json,
"controls": {
"search": search,
"sort": sort_json,
"pageSize": page_size,
},
});
serde_json::to_string(&payload)
.unwrap_or_else(|_| "{}".into())
.replace('<', "\\u003c")
}