use super::{ControlKind, PaletteOptions, RangeConfig, SelectOption};
use vize_carton::FxHashSet;
#[inline]
pub fn infer_control_type(value: &serde_json::Value) -> ControlKind {
match value {
serde_json::Value::Bool(_) => ControlKind::Boolean,
serde_json::Value::Number(_) => ControlKind::Number,
serde_json::Value::String(s) => infer_string_control(s),
serde_json::Value::Array(_) => ControlKind::Array,
serde_json::Value::Object(_) => ControlKind::Object,
serde_json::Value::Null => ControlKind::Text,
}
}
#[inline]
fn infer_string_control(s: &str) -> ControlKind {
if is_color_value(s) {
return ControlKind::Color;
}
if is_date_value(s) {
return ControlKind::Date;
}
ControlKind::Text
}
#[inline]
fn is_color_value(s: &str) -> bool {
if let Some(hex) = s.strip_prefix('#') {
let len = hex.len();
return (len == 3 || len == 4 || len == 6 || len == 8)
&& hex.chars().all(|c| c.is_ascii_hexdigit());
}
let lower = s.to_lowercase();
if lower.starts_with("rgb(")
|| lower.starts_with("rgba(")
|| lower.starts_with("hsl(")
|| lower.starts_with("hsla(")
{
return true;
}
matches!(
lower.as_str(),
"red"
| "green"
| "blue"
| "white"
| "black"
| "yellow"
| "orange"
| "purple"
| "pink"
| "gray"
| "grey"
| "cyan"
| "magenta"
| "transparent"
)
}
#[inline]
fn is_date_value(s: &str) -> bool {
if s.len() == 10 {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() == 3 {
return parts[0].len() == 4
&& parts[1].len() == 2
&& parts[2].len() == 2
&& parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()));
}
}
if s.len() >= 19 && s.contains('T') {
let parts: Vec<&str> = s.split('T').collect();
if parts.len() == 2 {
return is_date_value(parts[0]);
}
}
false
}
pub fn infer_control_from_values(
values: &[serde_json::Value],
options: &PaletteOptions,
) -> (ControlKind, Vec<SelectOption>, Option<RangeConfig>) {
if values.is_empty() {
return (ControlKind::Text, Vec::new(), None);
}
let base_type = values
.iter()
.find(|v| !v.is_null())
.map(infer_control_type)
.unwrap_or(ControlKind::Text);
if base_type == ControlKind::Text && options.infer_options {
let unique_values = collect_unique_strings(values);
let count = unique_values.len();
if count >= options.min_select_values && count <= options.max_select_values {
let select_options: Vec<SelectOption> = unique_values
.into_iter()
.map(|v| SelectOption {
label: humanize_label(&v),
value: serde_json::json!(v),
})
.collect();
return (ControlKind::Select, select_options, None);
}
}
if base_type == ControlKind::Number {
if let Some(range) = infer_number_range(values) {
return (ControlKind::Range, Vec::new(), Some(range));
}
}
(base_type, Vec::new(), None)
}
fn collect_unique_strings(values: &[serde_json::Value]) -> Vec<String> {
let mut seen = FxHashSet::default();
let mut result = Vec::new();
for value in values {
if let serde_json::Value::String(s) = value {
if seen.insert(s.clone()) {
result.push(s.clone());
}
}
}
result
}
fn infer_number_range(values: &[serde_json::Value]) -> Option<RangeConfig> {
let numbers: Vec<f64> = values.iter().filter_map(|v| v.as_f64()).collect();
if numbers.len() < 2 {
return None;
}
let min = numbers.iter().cloned().fold(f64::INFINITY, f64::min);
let max = numbers.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if (max - min).abs() < f64::EPSILON {
return None;
}
let mut diffs: Vec<f64> = numbers
.windows(2)
.map(|w| (w[1] - w[0]).abs())
.filter(|d| *d > f64::EPSILON)
.collect();
diffs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let step = diffs.first().copied();
let range_extend = (max - min) * 0.1;
Some(RangeConfig {
min: (min - range_extend).floor(),
max: (max + range_extend).ceil(),
step,
})
}
#[inline]
fn humanize_label(s: &str) -> String {
let result = s
.chars()
.fold(String::new(), |mut acc, c| {
if c.is_uppercase() && !acc.is_empty() {
acc.push(' ');
}
acc.push(c);
acc
});
let result = result.replace(['_', '-'], " ");
let mut chars = result.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_boolean() {
assert_eq!(
infer_control_type(&serde_json::json!(true)),
ControlKind::Boolean
);
assert_eq!(
infer_control_type(&serde_json::json!(false)),
ControlKind::Boolean
);
}
#[test]
fn test_infer_number() {
assert_eq!(
infer_control_type(&serde_json::json!(42)),
ControlKind::Number
);
assert_eq!(
infer_control_type(&serde_json::json!(1.5)),
ControlKind::Number
);
}
#[test]
fn test_infer_color() {
assert_eq!(
infer_control_type(&serde_json::json!("#ff0000")),
ControlKind::Color
);
assert_eq!(
infer_control_type(&serde_json::json!("#FFF")),
ControlKind::Color
);
assert_eq!(
infer_control_type(&serde_json::json!("rgb(255, 0, 0)")),
ControlKind::Color
);
assert_eq!(
infer_control_type(&serde_json::json!("red")),
ControlKind::Color
);
}
#[test]
fn test_infer_date() {
assert_eq!(
infer_control_type(&serde_json::json!("2024-01-15")),
ControlKind::Date
);
assert_eq!(
infer_control_type(&serde_json::json!("2024-01-15T10:30:00")),
ControlKind::Date
);
}
#[test]
fn test_infer_select_from_values() {
let values = vec![
serde_json::json!("sm"),
serde_json::json!("md"),
serde_json::json!("lg"),
];
let (kind, options, _) = infer_control_from_values(&values, &PaletteOptions::default());
assert_eq!(kind, ControlKind::Select);
assert_eq!(options.len(), 3);
}
#[test]
fn test_infer_range_from_values() {
let values = vec![
serde_json::json!(10),
serde_json::json!(20),
serde_json::json!(30),
serde_json::json!(40),
];
let (kind, _, range) = infer_control_from_values(&values, &PaletteOptions::default());
assert_eq!(kind, ControlKind::Range);
assert!(range.is_some());
}
#[test]
fn test_humanize_label() {
assert_eq!(humanize_label("primaryColor"), "Primary Color");
assert_eq!(humanize_label("font_size"), "Font size");
assert_eq!(humanize_label("is-active"), "Is active");
assert_eq!(humanize_label("sm"), "Sm");
}
}