use super::*;
use serde_json::Value;
#[derive(serde::Serialize)]
struct BomRow {
#[serde(rename = "partName")]
part_name: String,
#[serde(rename = "sourceKey")]
source_key: String,
quantity: usize,
}
fn csv_field(text: &str) -> String {
if text.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", text.replace('"', "\"\""))
} else {
text.to_string()
}
}
impl EngineState {
fn bom_rows(&mut self) -> Result<Vec<BomRow>, String> {
self.ensure_assembly_synced();
if self.assembly_components.is_empty() {
return Err("no components in the assembly".into());
}
let library: std::collections::BTreeMap<String, Value> =
serde_json::from_str(&brep_kernel::parts_library_json())
.map_err(|error| format!("parts library unreadable: {error}"))?;
Ok(library
.into_iter()
.map(|(part_name, entry)| {
let quantity = self
.assembly_components
.iter()
.filter(|record| record.part_name == part_name)
.count();
BomRow {
source_key: entry
.get("sourceKey")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
part_name,
quantity,
}
})
.filter(|row| row.quantity > 0)
.collect())
}
pub fn export_bom_csv(&mut self) -> Result<String, String> {
let mut out = String::from("partName,sourceKey,quantity\n");
for row in self.bom_rows()? {
out.push_str(&format!(
"{},{},{}\n",
csv_field(&row.part_name),
csv_field(&row.source_key),
row.quantity
));
}
Ok(out)
}
pub fn export_bom_json(&mut self) -> Result<String, String> {
serde_json::to_string(&self.bom_rows()?)
.map_err(|error| format!("BOM serialize: {error}"))
}
pub fn part_attributes(&self, part_name: &str) -> Value {
self.history
.parts_library()
.get(part_name)
.and_then(|entry| entry.get("document"))
.and_then(|document| document.get(PART_ATTRIBUTES))
.filter(|value| value.is_object())
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub fn part_source(&self, part_name: &str) -> Option<(String, String)> {
let entry = self.history.parts_library().get(part_name)?;
Some((
entry
.get("sourceKey")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
entry
.get("sourceSignature")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
))
}
pub fn part_document_json(&self, part_name: &str) -> Option<String> {
self.history
.parts_library()
.get(part_name)
.and_then(|entry| entry.get("document"))
.map(|document| document.to_string())
}
pub fn set_part_attribute(
&mut self,
part_name: &str,
key: &str,
value: Value,
) -> Result<(), String> {
if key.is_empty() {
return Err("part attribute: empty key".to_string());
}
let mut block = self.history.parts_library().clone();
let entry = block
.get_mut(part_name)
.ok_or_else(|| format!("no parts-library entry '{part_name}'"))?;
let document = entry
.get_mut("document")
.filter(|value| value.is_object())
.ok_or_else(|| format!("part '{part_name}': malformed document"))?;
write_attribute(document, PART_ATTRIBUTES, key, value)?;
let document_text = document.to_string();
let signature = super::document_signature(&document_text);
entry["sourceSignature"] = Value::String(signature.clone());
brep_kernel::refresh_library_entry(part_name, &signature, &document_text)
.map_err(|error| format!("part '{part_name}': {error:?}"))?;
self.history
.set_parts_library_edited(block, Some(&format!("partattr:{part_name}:{key}")));
self.rerun_history();
Ok(())
}
pub fn document_part_attributes(&self) -> Value {
self.history
.part_attributes_block()
.filter(|value| value.is_object())
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub fn set_document_part_attribute(
&mut self,
key: &str,
value: Value,
) -> Result<(), String> {
if key.is_empty() {
return Err("part attribute: empty key".to_string());
}
let mut owner = Value::Object(serde_json::Map::new());
if let Some(block) = self.history.part_attributes_block() {
let block = block.clone();
if let Some(object) = owner.as_object_mut() {
object.insert(PART_ATTRIBUTES.to_string(), block);
}
}
write_attribute(&mut owner, PART_ATTRIBUTES, key, value)?;
let block = owner
.as_object_mut()
.and_then(|object| object.remove(PART_ATTRIBUTES));
self.history
.set_part_attributes_block(block, Some(&format!("docpartattr:{key}")));
self.rerun_history();
Ok(())
}
pub fn occurrence_attributes(&self, component_id: &str) -> Value {
self.history
.index_of(component_id)
.and_then(|index| self.history.feature_params(index))
.and_then(|params| params.get(OCCURRENCE_ATTRIBUTES).cloned())
.filter(Value::is_object)
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub fn set_occurrence_attribute(
&mut self,
component_ids: &[String],
key: &str,
value: Value,
) -> Result<(), String> {
if key.is_empty() {
return Err("occurrence attribute: empty key".to_string());
}
let mut edits: Vec<(String, Value)> = Vec::with_capacity(component_ids.len());
for id in component_ids {
let index = self
.history
.index_of(id)
.ok_or_else(|| format!("no component feature '{id}'"))?;
let mut params = self
.history
.feature_params(index)
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
if !params.is_object() {
return Err(format!("component '{id}': malformed inputParams"));
}
write_attribute(&mut params, OCCURRENCE_ATTRIBUTES, key, value.clone())?;
edits.push((id.clone(), params));
}
self.update_many_feature_params(&edits)?;
Ok(())
}
}
pub const PART_ATTRIBUTES: &str = "partAttributes";
pub const OCCURRENCE_ATTRIBUTES: &str = "bom";
fn write_attribute(
owner: &mut Value,
record_key: &str,
key: &str,
value: Value,
) -> Result<(), String> {
let object = owner
.as_object_mut()
.ok_or_else(|| "attribute owner is not an object".to_string())?;
let clearing = matches!(&value, Value::Null)
|| matches!(&value, Value::String(text) if text.is_empty());
if clearing {
let mut empty = false;
if let Some(record) = object.get_mut(record_key).and_then(Value::as_object_mut) {
record.remove(key);
empty = record.is_empty();
}
if empty {
object.remove(record_key);
}
return Ok(());
}
let record = object
.entry(record_key.to_string())
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if !record.is_object() {
*record = Value::Object(serde_json::Map::new());
}
record
.as_object_mut()
.expect("just normalized to an object")
.insert(key.to_string(), value);
Ok(())
}