//! One operation resolver for admission, forms and scenario labels. use crate::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedOperation { /// The actual Call/Asynchronous declaration, also when resolving its Reply. pub invocation: Id, pub allocation: Id, pub interface: Id, pub item: Id, pub caller_port: Id, pub provider_port: Id, pub transmission: TransmissionProtocol, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum OperationResolution { Ordinary, Unresolved { invocation: Id, reason: String }, Selected(ResolvedOperation), } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct OperationError { pub code: &'static str, pub message: String, } fn error(code: &'static str, message: &str) -> OperationError { OperationError { code, message: message.into(), } } pub fn resolve(p: &Project, occurrence: Id) -> Result { let e = p .elements .get(&occurrence) .ok_or_else(|| error("operation_context", "The message no longer exists."))?; let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation, message_kind, reply_to, .. }, .. }) = e.shared() else { return Ok(OperationResolution::Ordinary); }; let invocation = if *message_kind == MessageKind::Reply { if operation.is_some() { return Err(error( "reply_operation", "A Reply derives its operation from its exact Call; it cannot store a second selection.", )); } let Some(call) = reply_to else { return Ok(OperationResolution::Ordinary); }; if !matches!( p.elements.get(call).and_then(Element::shared), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind: MessageKind::Call, .. }, .. }) ) { return Ok(OperationResolution::Ordinary); } *call } else { occurrence }; let Some(SharedBody::Occurrence { body: body @ OccurrenceBody::Exchange { operation: Some(operation), message_kind, exchange, sender, receiver, .. }, .. }) = p.elements.get(&invocation).and_then(Element::shared) else { return Ok(OperationResolution::Ordinary); }; if p.layer_of(invocation) != Some(Layer::Sa) || !matches!(message_kind, MessageKind::Call | MessageKind::Asynchronous) || validation::ancestor_of_kind(p, invocation, Kind::Scenario) .and_then(|id| p.elements.get(&id)) .and_then(Element::shared) .is_none_or(|body| { !matches!( body, SharedBody::Scenario { viewpoint: ScenarioViewpoint::BoundaryInterface, .. } ) }) { return Err(error( "operation_context", "Operation use belongs to a Call or Asynchronous message in an SA BoundaryInterface scenario.", )); } let unresolved = |reason: &str| { Ok(OperationResolution::Unresolved { invocation, reason: reason.into(), }) }; if exchange .and_then(|id| p.relations.get(&id)) .is_some_and(|r| r.kind() != RelationKind::BoundaryExchange) { return Err(error( "operation_channel", "Operation use requires an explicit boundary exchange channel.", )); } let Some(allocation) = operation.allocation else { return unresolved( "Choose the interface operation allocation; existing value expressions are preserved.", ); }; let Some(relation) = p.relations.get(&allocation) else { return Err(error( "operation_allocation", "The selected interface operation allocation no longer exists.", )); }; let RelationBody::InterfaceItemAllocation { source: interface, target: item, transmission, .. } = relation.body else { return Err(error( "operation_allocation", "Choose an InterfaceItemAllocation, not an item or another relationship.", )); }; if p.layer_of(allocation) != Some(Layer::Sa) || !matches!( p.elements.get(&item).and_then(Element::shared), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Information, mechanism: ExchangeMechanism::Operation, .. }) ) { return Err(error( "operation_allocation", "The selected SA allocation must declare an Information item with Operation mechanism.", )); } let Some(channel) = exchange.and_then(|id| p.relations.get(&id)) else { return Err(error( "operation_channel", "A selected operation requires an existing boundary exchange carrying its item; clear the allocation to retain unresolved intent.", )); }; if channel.kind() != RelationKind::BoundaryExchange || !channel .body .items() .iter() .any(|carried| data::item_satisfies(p, *carried, item)) { return Err(error( "operation_channel", "The boundary exchange must carry the selected operation item or an admitted subtype.", )); } if sender.is_none() || receiver.is_none() { return Err(error( "operation_lifelines", "A selected operation requires actual caller and provider lifelines; clear the allocation to retain unresolved intent.", )); } let (caller_port, provider_port) = match validation::resolve_message_endpoints(p, body) { validation::MessageEndpointResolution::Resolved { source, target } => (source, target), validation::MessageEndpointResolution::Conflict => { return Err(error( "operation_direction", "The selected lifelines conflict with the boundary channel's actual caller/provider direction.", )); } _ => { return Err(error( "operation_direction", "A selected operation requires a proven actual caller/provider channel direction; clear the allocation to retain unresolved intent.", )); } }; for port in [caller_port, provider_port] { if !matches!( p.elements.get(&port).and_then(ports::port_contract), Some(PortContract::Component { kind: ComponentPortKind::Standard, .. }) ) { return Err(error( "operation_ports", "Operation proof requires concrete Standard component ports.", )); } } let exposes = |interface| { data::effective_interface_allocations(p, interface) .iter() .any(|r| r.id == allocation) }; let providers: Vec<_> = p .relations .values() .filter(|r| { r.kind() == RelationKind::InterfaceProvision && r.source() == provider_port && exposes(r.target()) }) .map(Relation::target) .collect(); let valid = p.relations.values().any(|r| { r.kind() == RelationKind::InterfaceRequirement && r.source() == caller_port && exposes(r.target()) && providers .iter() .any(|provided| data::interface_satisfies(p, *provided, r.target())) }); if !valid { return Err(error( "operation_roles", "The actual receiver port must provide, and sender port require, compatible interfaces exposing this exact allocation.", )); } if !matches!( (message_kind, transmission), (_, TransmissionProtocol::Unspecified) | (MessageKind::Call, TransmissionProtocol::Synchronous) | ( MessageKind::Asynchronous, TransmissionProtocol::Asynchronous ) ) { return Err(error( "operation_transmission", "Calls require synchronous transmission; asynchronous operation messages require asynchronous transmission.", )); } let selected = ResolvedOperation { invocation, allocation, interface, item, caller_port, provider_port, transmission, }; if occurrence != invocation { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: reply_exchange, .. }, .. }) = e.shared() else { unreachable!() }; if let Some(response) = reply_exchange.and_then(|id| p.relations.get(&id)) && response.id != channel.id && !response .body .items() .iter() .any(|carried| data::item_satisfies(p, *carried, item)) { return Err(error( "operation_reply_channel", "A separate reply channel must carry the Call's selected operation item or an admitted subtype, including parameterless returns.", )); } } Ok(OperationResolution::Selected(selected)) } /// Valid selections under the proposed message context, without mutating the model. pub fn eligible_allocations(p: &Project, occurrence: Id) -> Vec { let mut candidate = p.clone(); let ids: Vec<_> = p .relations .values() .filter(|r| r.kind() == RelationKind::InterfaceItemAllocation) .map(|r| r.id) .collect(); ids.into_iter() .filter(|id| { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation, .. }, .. }) = candidate .elements .get_mut(&occurrence) .and_then(Element::shared_mut) else { return false; }; *operation = Some(OperationUse { allocation: Some(*id), }); matches!( resolve(&candidate, occurrence), Ok(OperationResolution::Selected(_)) ) }) .collect() } /// Reuse the existing bounded field-path engine with exactly one selected item. pub fn signature_relation(operation: &ResolvedOperation) -> Relation { Relation { id: operation.allocation, alias: String::new(), name: String::new(), description: String::new(), body: RelationBody::Interaction { source: operation.caller_port, target: operation.provider_port, items: vec![operation.item], }, } }