app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
Drew Owens
Share frozen native Rust mechanical development checkpoint and qualification scope
b2d7cdb
26d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
use crate::{Error, Result, SourceLink, Target};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfigurationWitness {
pub target: Target,
pub configured: bool,
pub is_configuration: bool,
pub configuration_row_id: Option<String>,
pub complete: bool,
pub unsupported: Vec<String>,
pub read_errors: Vec<String>,
/// Nonempty means externally configured inserted occurrences; unsupported
/// in the legacy complete-BOM contract and deliberately still refused.
pub configured_occurrences: Vec<ConfiguredOccurrence>,
pub table: Option<ConfigurationTable>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfiguredOccurrence {
pub occurrence_id: String,
pub path: String,
pub table_id: String,
pub row_id: String,
pub row_name: String,
pub source_file_id: String,
pub source_version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfigurationTable {
pub id: String,
pub active_row_id: String,
pub active_row_name: String,
pub has_custom_columns: bool,
pub theme_table_ids: Vec<String>,
/// Includes the optional reserved test row; it cannot be the active row.
pub rows: Vec<ConfigurationRow>,
pub columns: Vec<ConfigurationColumn>,
pub cells: Vec<ConfigurationCell>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RowScope {
#[serde(rename = "ordinary")]
Ordinary,
#[serde(rename = "test")]
Test,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfigurationRow {
pub id: String,
pub name: String,
pub scope: RowScope,
pub parent_id: Option<String>,
pub child_ids: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ColumnKind {
Parameter,
Suppress,
Visibility,
Property,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfigurationColumn {
pub id: String,
pub kind: ColumnKind,
pub source: Option<SourceLink>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CellValue {
ConfigurationSuppressCell {
#[serde(rename = "isSuppressed")]
is_suppressed: bool,
#[serde(rename = "isEffectivelySuppressed")]
is_effectively_suppressed: bool,
},
ConfigurationVisibilityCell {
#[serde(rename = "isVisible")]
is_visible: bool,
},
/// The native adapter's complete configuration witness must separately
/// validate parameter value types/units. BOM only needs its cell address.
ConfigurationParameterCell {
expression: String,
},
ConfigurationPropertyCell {
value: String,
},
}
impl CellValue {
fn kind(&self) -> ColumnKind {
match self {
Self::ConfigurationSuppressCell { .. } => ColumnKind::Suppress,
Self::ConfigurationVisibilityCell { .. } => ColumnKind::Visibility,
Self::ConfigurationParameterCell { .. } => ColumnKind::Parameter,
Self::ConfigurationPropertyCell { .. } => ColumnKind::Property,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigurationCell {
pub row_id: String,
pub column_id: String,
pub scope: RowScope,
#[serde(flatten)]
pub value: CellValue,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigurationProvenance {
pub configured: bool,
pub is_configuration: bool,
pub table_id: Option<String>,
pub row_id: Option<String>,
pub row_name: Option<String>,
pub scope: String,
pub graph_complete: bool,
}
pub(crate) struct ConfigurationContext<'a> {
pub provenance: ConfigurationProvenance,
pub suppression_cells: Vec<ConfigurationCell>,
pub suppressed_sources: Vec<&'a SourceLink>,
}
impl ConfigurationWitness {
pub(crate) fn validate(&self, target: &Target) -> Result<ConfigurationContext<'_>> {
let unsupported = |message: &str| Error::new("configuration_scope_unsupported", message);
let invalid = |message: &str| Error::new("configuration_read_failed", message);
if &self.target != target {
return Err(Error::new(
"configuration_source_mismatch",
"Configuration witness belongs to another target/revision",
));
}
if !self.complete
|| !self.unsupported.is_empty()
|| !self.read_errors.is_empty()
|| self.is_configuration
|| !self.configured_occurrences.is_empty()
{
return Err(unsupported(
"A complete supported configuration witness is required for BOM generation",
));
}
if self
.configuration_row_id
.as_ref()
.is_some_and(|id| !id.is_empty())
{
return Err(invalid("Native configuration flag and row ID disagree"));
}
if self.configured != self.table.is_some() {
return Err(invalid(
"Configured-design flag and table inventory disagree",
));
}
let mut context = ConfigurationContext {
provenance: ConfigurationProvenance {
configured: self.configured,
is_configuration: false,
table_id: None,
row_id: None,
row_name: None,
scope: "current active configuration only".into(),
graph_complete: true,
},
suppression_cells: Vec::new(),
suppressed_sources: Vec::new(),
};
let Some(table) = &self.table else {
return Ok(context);
};
if table.has_custom_columns || !table.theme_table_ids.is_empty() {
return Err(unsupported(
"Custom columns and theme-table contents remain unsupported",
));
}
if table.rows.is_empty()
|| table.rows.len() > 128
|| table.columns.len() > 128
|| table.rows.len().saturating_mul(table.columns.len()) > 4096
|| table.cells.len() > 4096
{
return Err(Error::new(
"configuration_budget",
"Configuration inventory exceeds its budget",
));
}
if table.id.is_empty() {
return Err(invalid("Configuration table ID is missing"));
}
let mut rows = BTreeMap::new();
for row in &table.rows {
if row.id.is_empty() || rows.insert(row.id.as_str(), row).is_some() {
return Err(invalid("Missing or duplicate configuration row IDs"));
}
if row.parent_id.is_some() || !row.child_ids.is_empty() {
return Err(unsupported(
"Hierarchical configuration rows remain unsupported",
));
}
}
if rows
.values()
.filter(|row| row.scope == RowScope::Test)
.count()
> 1
{
return Err(invalid("Multiple reserved test rows"));
}
let active = rows
.get(table.active_row_id.as_str())
.ok_or_else(|| invalid("Active row is outside ordinary rows"))?;
if active.scope != RowScope::Ordinary || active.name != table.active_row_name {
return Err(invalid("Active row identity/name is inconsistent"));
}
let mut columns = BTreeMap::new();
for column in &table.columns {
if column.id.is_empty() || columns.insert(column.id.as_str(), column).is_some() {
return Err(invalid("Missing or duplicate configuration column IDs"));
}
if column.kind != ColumnKind::Property && column.source.is_none() {
return Err(invalid(
"Configuration controlled-source witness is missing",
));
}
}
let mut addresses = BTreeSet::new();
for cell in &table.cells {
let row = rows
.get(cell.row_id.as_str())
.ok_or_else(|| invalid("Cell row is outside table"))?;
let column = columns
.get(cell.column_id.as_str())
.ok_or_else(|| invalid("Cell column is outside table"))?;
if !addresses.insert((&cell.row_id, &cell.column_id))
|| cell.scope != row.scope
|| cell.value.kind() != column.kind
{
return Err(invalid("Cell address, scope, or type is inconsistent"));
}
if cell.row_id == table.active_row_id {
if let CellValue::ConfigurationSuppressCell {
is_effectively_suppressed,
..
} = cell.value
{
context.suppression_cells.push(cell.clone());
if is_effectively_suppressed {
context
.suppressed_sources
.push(column.source.as_ref().expect("source checked"));
}
}
}
}
if addresses.len() != rows.len() * columns.len() {
return Err(invalid("Configuration cell inventory is incomplete"));
}
context.provenance.table_id = Some(table.id.clone());
context.provenance.row_id = Some(table.active_row_id.clone());
context.provenance.row_name = Some(table.active_row_name.clone());
Ok(context)
}
}