1use serde_json::Value;
2
3use crate::models::{GridColumnDef, GridRecord};
4
5const PROTECTED_PATH_SEGMENTS: &[&str] = &["__proto__", "constructor", "prototype"];
6
7pub fn is_null_or_undefined(value: &Value) -> bool {
8 value.is_null()
9}
10
11pub fn get_path_value(record: &GridRecord, path: &str) -> Option<Value> {
12 let mut current = record;
13
14 for part in path.split('.') {
15 if PROTECTED_PATH_SEGMENTS.contains(&part) {
16 return None;
17 }
18
19 match current {
20 Value::Object(map) => {
21 current = map.get(part)?;
22 }
23 _ => return None,
24 }
25 }
26
27 Some(current.clone())
28}
29
30pub fn get_cell_value(row: &GridRecord, column: &GridColumnDef) -> Value {
31 if let Some(field) = &column.field {
32 return get_path_value(row, field).unwrap_or(Value::Null);
33 }
34
35 match row {
36 Value::Object(map) => map.get(&column.name).cloned().unwrap_or(Value::Null),
37 _ => Value::Null,
38 }
39}
40
41pub fn stringify_cell_value(value: &Value) -> String {
42 match value {
43 Value::Null => String::new(),
44 Value::Bool(value) => value.to_string(),
45 Value::Number(value) => value.to_string(),
46 Value::String(value) => value.clone(),
47 Value::Array(values) => values
48 .iter()
49 .map(stringify_cell_value)
50 .collect::<Vec<_>>()
51 .join(", "),
52 Value::Object(_) => serde_json::to_string(value).unwrap_or_default(),
53 }
54}
55
56pub fn to_csv_value(value: &str) -> String {
57 let mut escaped = value.to_string();
58
59 if matches!(
60 escaped.chars().next(),
61 Some('=' | '+' | '-' | '@' | '\t' | '\r')
62 ) {
63 escaped = format!("'{}", escaped);
64 }
65
66 if escaped.contains([',', '"', '\n']) {
67 return format!("\"{}\"", escaped.replace('"', "\"\""));
68 }
69
70 escaped
71}
72
73pub fn titleize(value: &str) -> String {
74 let mut output = String::with_capacity(value.len());
75 let mut previous_is_lower_or_digit = false;
76
77 for ch in value.chars() {
78 if matches!(ch, '_' | '.' | '-') {
79 if !output.ends_with(' ') {
80 output.push(' ');
81 }
82 previous_is_lower_or_digit = false;
83 continue;
84 }
85
86 if previous_is_lower_or_digit && ch.is_ascii_uppercase() && !output.ends_with(' ') {
87 output.push(' ');
88 }
89
90 output.push(ch);
91 previous_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
92 }
93
94 let compact = output.split_whitespace().collect::<Vec<_>>().join(" ");
95 let mut chars = compact.chars();
96 match chars.next() {
97 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
98 None => String::new(),
99 }
100}