use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const FAMILY_TABLE_KEY: &str = "familyTable";
pub const FAMILY_SOURCE_KEY: &str = "familySource";
pub const TEMPLATE_SOURCE_KEY: &str = "templateSource";
pub const DERIVED_FROM_KEY: &str = "derivedFrom";
pub const DOCUMENT_CLASS_KEY: &str = "documentClass";
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FamilyTable {
#[serde(default)]
pub columns: Vec<FamilyColumn>,
#[serde(default)]
pub rows: Vec<FamilyRow>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FamilyColumn {
pub name: String,
#[serde(default)]
pub label: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FamilyRow {
pub part_number: String,
#[serde(default)]
pub revision: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub values: BTreeMap<String, String>,
}
pub fn effective_row(table: &FamilyTable, row: &FamilyRow) -> FamilyRow {
let mut row = row.clone();
row.values.retain(|name, value| {
!value.trim().is_empty() && table.columns.iter().any(|column| column.name == *name)
});
row
}
pub fn rewrite_expressions(expressions: &str, values: &BTreeMap<String, String>) -> String {
let mut out = expressions.to_string();
let mut defined = std::collections::BTreeSet::new();
for (start, end) in statement_spans(expressions).into_iter().rev() {
let Some(name) = assigned_name(&expressions[start..end]) else {
continue;
};
if let Some(value) = values.get(name) {
out.replace_range(start..end, &format!("{name} = {}", value.trim()));
defined.insert(name.to_string());
}
}
for (name, value) in values {
if !defined.contains(name.as_str()) {
if !out.is_empty() && !out.trim_end().ends_with(';') {
out.push(';');
}
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&format!("{name} = {};\n", value.trim()));
}
}
out
}
fn statement_spans(source: &str) -> Vec<(usize, usize)> {
let bytes = source.as_bytes();
let mut spans = Vec::new();
let mut start = None;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'/' if bytes.get(i + 1) == Some(&b'/') => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
continue;
}
b';' => {
if let Some(s) = start.take() {
spans.push((s, i));
}
}
c if !c.is_ascii_whitespace() && start.is_none() => start = Some(i),
_ => {}
}
i += 1;
}
if let Some(s) = start {
spans.push((s, source.len()));
}
spans
}
fn assigned_name(statement: &str) -> Option<&str> {
let eq = statement.find('=')?;
let name = statement[..eq].trim();
is_identifier(name).then_some(name)
}
pub fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}
pub const NOT_THE_MODEL: [&str; 7] = [
FAMILY_TABLE_KEY,
DERIVED_FROM_KEY,
DOCUMENT_CLASS_KEY,
"workbench",
FAMILY_SOURCE_KEY,
TEMPLATE_SOURCE_KEY,
"thumbnail",
];
pub fn history_hash(family: &Value) -> String {
let mut model = family.clone();
if let Some(object) = model.as_object_mut() {
for key in NOT_THE_MODEL {
object.remove(key);
}
}
format!("{:016x}", super::parts_library::stable_json_hash(&model))
}
pub fn row_hash(row: &FamilyRow) -> String {
let value = serde_json::json!({
"partNumber": row.part_number.trim(),
"description": row.description.trim(),
"values": row.values,
});
format!("{:016x}", super::parts_library::stable_json_hash(&value))
}