use uuid::Uuid;
use super::export::{
self, ComponentScope, DecisionAggregate, Element, ElementPayload, Export, ExternalId,
SubjectRef,
};
use super::sidecar::DecisionFacts;
use super::tree::{NodeIds, node_id};
use super::{Conversion, ConvertError, label};
use crate::decision::{
Assessment, Consequence, Decision, DecisionOption, DecisionState, Driver, DriverPayload,
NodeRef,
};
use crate::document::Edge;
use crate::identity::{Key, Label};
use crate::ids::{ConsequenceId, DecisionId, DriverId, NodeId, OptionId};
use crate::slug::Slug;
pub(super) fn convert(
export: &Export,
nodes: &NodeIds,
conversion: &mut Conversion,
) -> Result<(), ConvertError> {
let edges = &mut conversion.document.edges;
edges.extend(realizations(export, nodes)?);
edges.extend(scopes(export, nodes)?);
for aggregate in &export.architecture_decisions {
decision(aggregate, nodes, conversion)?;
}
Ok(())
}
fn realizations(export: &Export, nodes: &NodeIds) -> Result<Vec<Edge>, ConvertError> {
export
.realization_links
.iter()
.map(
|link| match (nodes.get(&link.feature_id), nodes.get(&link.component_id)) {
(Some(NodeId::Feature(feature)), Some(NodeId::Component(component))) => {
Ok(Edge::Realizes {
feature: *feature,
component: *component,
})
}
_ => Err(ConvertError::Link {
feature: link.feature_id,
component: link.component_id,
}),
},
)
.collect()
}
fn scopes(export: &Export, nodes: &NodeIds) -> Result<Vec<Edge>, ConvertError> {
let mut edges = Vec::new();
for node in &export.nodes {
let Some(ComponentScope::Product { product_id }) = node.component_scope else {
continue;
};
match (nodes.get(&node.node_id), nodes.get(&product_id)) {
(Some(NodeId::Component(component)), Some(NodeId::Product(product))) => {
edges.push(Edge::ScopedTo {
component: *component,
product: *product,
});
}
_ => {
return Err(ConvertError::Scope {
node: node.external_id.clone(),
product: product_id,
});
}
}
}
Ok(edges)
}
fn decision(
aggregate: &DecisionAggregate,
nodes: &NodeIds,
conversion: &mut Conversion,
) -> Result<(), ConvertError> {
let mut reader = Reader {
aggregate,
nodes,
facts: DecisionFacts::default(),
former: Vec::new(),
};
let decision = reader.decision()?;
for link in &aggregate.subject_links {
let subject = reader.subject(&link.subject)?;
conversion.document.edges.push(Edge::Subject {
decision: decision.id,
subject,
role: link.role,
});
}
let external_id = &aggregate.identity.external_id;
conversion
.document
.bindings
.bind(decision.id, label(external_id)?)?;
conversion.sidecar.former_labels.extend(reader.former);
conversion
.sidecar
.decisions
.insert(decision.id, reader.facts);
conversion.document.decisions.push(decision);
Ok(())
}
struct Reader<'a> {
aggregate: &'a DecisionAggregate,
nodes: &'a NodeIds,
facts: DecisionFacts,
former: Vec<(Label, Uuid)>,
}
impl<'a> Reader<'a> {
fn external_id(&self) -> ExternalId {
self.aggregate.identity.external_id.clone()
}
fn element_error(&self, element: Uuid, reason: &'static str) -> ConvertError {
ConvertError::DecisionElement {
decision: self.external_id(),
element,
reason,
}
}
fn subject(&mut self, subject: &SubjectRef) -> Result<NodeRef, ConvertError> {
if let Some(id) = self.nodes.get(&subject.node_id) {
return Ok(NodeRef::Held(*id));
}
if node_id(subject.node_type, subject.tier, subject.node_id).is_none() {
return Err(ConvertError::DecisionSubject {
decision: self.external_id(),
subject: subject.node_id,
});
}
let former = label(&subject.external_id)?;
if !self.former.iter().any(|(known, _)| *known == former) {
self.former.push((former, subject.node_id));
}
Ok(NodeRef::Former(former))
}
fn elements(&self) -> Result<(&'a Element, Vec<&'a Element>), ConvertError> {
let aggregate = self.aggregate;
let id = aggregate.identity.decision_id;
let mut root = None;
let mut members = Vec::new();
for element in &aggregate.elements {
match element.parent_id {
None if element.id == id && root.is_none() => root = Some(element),
Some(parent) if parent == id => members.push(element),
_ => {
return Err(
self.element_error(element.id, "is neither the decision nor its child")
);
}
}
}
let root = root.ok_or_else(|| self.element_error(id, "is missing"))?;
members.sort_by_key(|element| element.ordinal);
Ok((root, members))
}
fn decision(&mut self) -> Result<Decision, ConvertError> {
let (root, members) = self.elements()?;
let ElementPayload::ArchitectureDecision {
title,
context,
state,
} = &root.payload
else {
return Err(self.element_error(root.id, "is not an architecture decision"));
};
self.facts.ordinal = root.ordinal;
let mut decision = Decision {
id: DecisionId::new(root.id.into()),
slug: Slug::derive(title, "decision"),
title: title.clone(),
context: context.clone(),
state: decision_state(state),
drivers: Vec::new(),
options: Vec::new(),
assessments: self.assessments(),
consequences: Vec::new(),
};
for member in members {
self.add_member(&mut decision, member)?;
}
Ok(decision)
}
fn add_member(
&mut self,
decision: &mut Decision,
member: &Element,
) -> Result<(), ConvertError> {
let key = Key::from(member.id);
match &member.payload {
ElementPayload::ArchitectureDecision { .. } => {
return Err(self.element_error(member.id, "is a second decision"));
}
ElementPayload::DecisionOption { title, description } => {
let id = OptionId::new(key);
self.facts.options.insert(id, member.ordinal);
decision.options.push(DecisionOption {
id,
title: title.clone(),
description: description.clone(),
});
}
ElementPayload::Driver(payload) => {
let id = DriverId::new(key);
self.facts.drivers.insert(id, member.ordinal);
let payload = match payload {
export::DriverPayload::Local { label, statement } => DriverPayload::Local {
label: label.clone(),
statement: statement.clone(),
},
export::DriverPayload::SpecificationSubject { subject } => {
DriverPayload::Subject {
subject: self.subject(subject)?,
}
}
};
decision.drivers.push(Driver { id, payload });
}
ElementPayload::Consequence {
kind,
statement,
subject,
} => {
let id = ConsequenceId::new(key);
self.facts.consequences.insert(id, member.ordinal);
let subject = subject
.as_ref()
.map(|subject| self.subject(subject))
.transpose()?;
decision.consequences.push(Consequence {
id,
kind: *kind,
statement: statement.clone(),
subject,
});
}
}
Ok(())
}
fn assessments(&self) -> Vec<Assessment> {
self.aggregate
.assessments
.iter()
.map(|assessment| Assessment {
option: OptionId::new(assessment.option_id.into()),
driver: DriverId::new(assessment.driver_id.into()),
rating: assessment.rating,
rationale: assessment.rationale.clone(),
})
.collect()
}
}
fn decision_state(state: &export::DecisionState) -> DecisionState {
let option = |id: &Uuid| OptionId::new((*id).into());
match state {
export::DecisionState::Proposed => DecisionState::Proposed,
export::DecisionState::Accepted {
selected_option_id,
outcome,
} => DecisionState::Accepted {
selected_option: option(selected_option_id),
outcome: outcome.clone(),
},
export::DecisionState::Rejected { rationale } => DecisionState::Rejected {
rationale: rationale.clone(),
},
export::DecisionState::Superseded {
selected_option_id,
outcome,
successor_decision_id,
} => DecisionState::Superseded {
selected_option: option(selected_option_id),
outcome: outcome.clone(),
successor: DecisionId::new((*successor_decision_id).into()),
},
export::DecisionState::Retired {
selected_option_id,
outcome,
rationale,
} => DecisionState::Retired {
selected_option: option(selected_option_id),
outcome: outcome.clone(),
rationale: rationale.clone(),
},
}
}