use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TableModel {
#[default]
Tatr,
SlanetWired,
SlanetWireless,
SlanetPlus,
SlanetAuto,
Disabled,
}
impl std::str::FromStr for TableModel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"tatr" => Ok(Self::Tatr),
"slanet_wired" => Ok(Self::SlanetWired),
"slanet_wireless" => Ok(Self::SlanetWireless),
"slanet_plus" => Ok(Self::SlanetPlus),
"slanet_auto" => Ok(Self::SlanetAuto),
"disabled" => Ok(Self::Disabled),
other => Err(format!(
"unknown table model: '{other}'. Valid: tatr, slanet_wired, slanet_wireless, slanet_plus, slanet_auto, disabled"
)),
}
}
}
impl fmt::Display for TableModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TableModel::Tatr => write!(f, "tatr"),
TableModel::SlanetWired => write!(f, "slanet_wired"),
TableModel::SlanetWireless => write!(f, "slanet_wireless"),
TableModel::SlanetPlus => write!(f, "slanet_plus"),
TableModel::SlanetAuto => write!(f, "slanet_auto"),
TableModel::Disabled => write!(f, "disabled"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TableOverlapPreference {
#[default]
Content,
Native,
Layout,
}
impl std::str::FromStr for TableOverlapPreference {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"content" => Ok(Self::Content),
"native" => Ok(Self::Native),
"layout" => Ok(Self::Layout),
other => Err(format!(
"unknown table overlap preference: '{other}'. Valid: content, native, layout"
)),
}
}
}
impl fmt::Display for TableOverlapPreference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TableOverlapPreference::Content => write!(f, "content"),
TableOverlapPreference::Native => write!(f, "native"),
TableOverlapPreference::Layout => write!(f, "layout"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayoutStrategy {
#[default]
Always,
Auto,
}
impl std::str::FromStr for LayoutStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"always" => Ok(Self::Always),
"auto" => Ok(Self::Auto),
other => Err(format!("unknown layout strategy: '{other}'. Valid: always, auto")),
}
}
}
impl fmt::Display for LayoutStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LayoutStrategy::Always => write!(f, "always"),
LayoutStrategy::Auto => write!(f, "auto"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutDetectionConfig {
#[serde(default)]
pub strategy: LayoutStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence_threshold: Option<f32>,
#[serde(default = "default_true")]
pub apply_heuristics: bool,
#[serde(default)]
pub table_model: TableModel,
#[serde(default)]
pub table_overlap_preference: TableOverlapPreference,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acceleration: Option<super::acceleration::AccelerationConfig>,
#[serde(default)]
pub enable_chart_understanding: bool,
}
impl Default for LayoutDetectionConfig {
fn default() -> Self {
Self {
strategy: LayoutStrategy::default(),
confidence_threshold: None,
apply_heuristics: true,
table_model: TableModel::default(),
table_overlap_preference: TableOverlapPreference::default(),
acceleration: None,
enable_chart_understanding: false,
}
}
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = LayoutDetectionConfig::default();
assert_eq!(config.strategy, LayoutStrategy::Always);
assert_eq!(config.table_model, TableModel::Tatr);
assert!(config.apply_heuristics);
assert!(config.confidence_threshold.is_none());
}
#[test]
fn layout_strategy_defaults_to_always_when_field_absent() {
let config: LayoutDetectionConfig = serde_json::from_str("{}").expect("empty config must deserialize");
assert_eq!(config.strategy, LayoutStrategy::Always);
}
#[test]
fn layout_strategy_serde_roundtrip_is_snake_case() {
let auto: LayoutStrategy = serde_json::from_str(r#""auto""#).expect("auto must deserialize");
assert_eq!(auto, LayoutStrategy::Auto);
assert_eq!(serde_json::to_string(&auto).expect("auto must serialize"), r#""auto""#);
let always: LayoutStrategy = serde_json::from_str(r#""always""#).expect("always must deserialize");
assert_eq!(always, LayoutStrategy::Always);
assert_eq!(
serde_json::to_string(&always).expect("always must serialize"),
r#""always""#
);
}
#[test]
fn layout_strategy_deserializes_from_toml_config() {
let config: LayoutDetectionConfig =
toml::from_str("strategy = \"auto\"").expect("toml config must deserialize");
assert_eq!(config.strategy, LayoutStrategy::Auto);
}
#[test]
fn layout_strategy_from_str_accepts_wire_names_and_rejects_unknown() {
assert_eq!("always".parse::<LayoutStrategy>(), Ok(LayoutStrategy::Always));
assert_eq!("auto".parse::<LayoutStrategy>(), Ok(LayoutStrategy::Auto));
let error = "adaptive".parse::<LayoutStrategy>().expect_err("unknown must fail");
assert!(error.contains("unknown layout strategy: 'adaptive'"));
assert!(error.contains("always, auto"));
}
#[test]
fn layout_strategy_display_matches_wire_format() {
assert_eq!(LayoutStrategy::Always.to_string(), "always");
assert_eq!(LayoutStrategy::Auto.to_string(), "auto");
}
#[test]
fn test_table_model_deserialize() {
let json = r#""tatr""#;
let model: TableModel = serde_json::from_str(json).unwrap();
assert_eq!(model, TableModel::Tatr);
let json = r#""slanet_auto""#;
let model: TableModel = serde_json::from_str(json).unwrap();
assert_eq!(model, TableModel::SlanetAuto);
let json = r#""disabled""#;
let model: TableModel = serde_json::from_str(json).unwrap();
assert_eq!(model, TableModel::Disabled);
}
#[test]
fn test_table_model_serialize() {
let json = serde_json::to_string(&TableModel::SlanetWired).unwrap();
assert_eq!(json, r#""slanet_wired""#);
}
#[test]
fn test_table_model_round_trip() {
for model in [
TableModel::Tatr,
TableModel::SlanetWired,
TableModel::SlanetWireless,
TableModel::SlanetPlus,
TableModel::SlanetAuto,
TableModel::Disabled,
] {
let serialized = serde_json::to_string(&model).unwrap();
let parsed: TableModel = serde_json::from_str(&serialized).unwrap();
assert_eq!(parsed, model, "round-trip failed for {model:?}");
}
}
#[test]
fn test_backward_compat_unknown_fields_ignored() {
let json = r#"{"preset": "accurate", "apply_heuristics": true}"#;
let config: LayoutDetectionConfig = serde_json::from_str(json).unwrap();
assert!(config.apply_heuristics);
assert_eq!(config.table_model, TableModel::Tatr);
}
#[test]
fn test_backward_compat_old_table_model_field() {
let json = r#"{"table_model": "slanet_wired"}"#;
let config: LayoutDetectionConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.table_model, TableModel::SlanetWired);
}
#[test]
fn test_table_model_display() {
assert_eq!(TableModel::Tatr.to_string(), "tatr");
assert_eq!(TableModel::SlanetWired.to_string(), "slanet_wired");
assert_eq!(TableModel::Disabled.to_string(), "disabled");
}
#[test]
fn layout_detection_config_omitting_enable_chart_understanding_defaults_to_false() {
let json = r#"{"apply_heuristics": true, "table_model": "tatr"}"#;
let config: LayoutDetectionConfig = serde_json::from_str(json).unwrap();
assert!(
!config.enable_chart_understanding,
"omitted enable_chart_understanding must default to false"
);
}
#[test]
fn table_overlap_preference_defaults_to_content() {
let config = LayoutDetectionConfig::default();
assert_eq!(config.table_overlap_preference, TableOverlapPreference::Content);
}
#[test]
fn table_overlap_preference_omitted_defaults_to_content() {
let json = r#"{"apply_heuristics": true, "table_model": "tatr"}"#;
let config: LayoutDetectionConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.table_overlap_preference, TableOverlapPreference::Content);
}
#[test]
fn table_overlap_preference_serde_snake_case() {
let config = LayoutDetectionConfig {
table_overlap_preference: TableOverlapPreference::Native,
..LayoutDetectionConfig::default()
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains(r#""table_overlap_preference":"native""#), "got: {json}");
let parsed: LayoutDetectionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.table_overlap_preference, TableOverlapPreference::Native);
}
#[test]
fn table_overlap_preference_from_str_and_display_round_trip() {
for pref in [
TableOverlapPreference::Content,
TableOverlapPreference::Native,
TableOverlapPreference::Layout,
] {
let s = pref.to_string();
let parsed: TableOverlapPreference = s.parse().unwrap();
assert_eq!(parsed, pref, "round-trip failed for {pref:?}");
}
assert!("bogus".parse::<TableOverlapPreference>().is_err());
}
#[test]
fn layout_detection_config_enable_chart_understanding_round_trip() {
let config = LayoutDetectionConfig {
enable_chart_understanding: true,
..LayoutDetectionConfig::default()
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: LayoutDetectionConfig = serde_json::from_str(&json).unwrap();
assert!(deserialized.enable_chart_understanding);
}
}