Files
ARCADIA-Studio/crates/oa-core/src/model.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

2016 lines
65 KiB
Rust

use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use uuid::Uuid;
pub type Id = Uuid;
pub const SCHEMA_VERSION: u32 = 7;
pub const MAX_COORDINATE: f32 = 1_000_000.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Layer {
Oa,
Sa,
}
impl fmt::Display for Layer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Oa => "OA",
Self::Sa => "SA",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Kind {
Entity,
Actor,
Role,
Activity,
Capability,
OperationalMission,
Process,
System,
SystemActor,
Function,
SystemCapability,
Mission,
FunctionalChain,
ExchangeItem,
ExchangeCategory,
DataType,
DataField,
Interface,
Port,
Scenario,
Lifeline,
Occurrence,
StateMachine,
Region,
State,
Mode,
Initial,
Final,
Choice,
Junction,
Fork,
Join,
ShallowHistory,
DeepHistory,
EntryPoint,
ExitPoint,
Terminate,
Event,
Constraint,
}
impl Kind {
pub const ALL: [Self; 39] = [
Self::Entity,
Self::Actor,
Self::Role,
Self::Activity,
Self::Capability,
Self::OperationalMission,
Self::Process,
Self::System,
Self::SystemActor,
Self::Function,
Self::SystemCapability,
Self::Mission,
Self::FunctionalChain,
Self::ExchangeItem,
Self::ExchangeCategory,
Self::DataType,
Self::DataField,
Self::Interface,
Self::Port,
Self::Scenario,
Self::Lifeline,
Self::Occurrence,
Self::StateMachine,
Self::Region,
Self::State,
Self::Mode,
Self::Initial,
Self::Final,
Self::Choice,
Self::Junction,
Self::Fork,
Self::Join,
Self::ShallowHistory,
Self::DeepHistory,
Self::EntryPoint,
Self::ExitPoint,
Self::Terminate,
Self::Event,
Self::Constraint,
];
pub fn label(self) -> &'static str {
match self {
Self::Entity => "Entity",
Self::Actor => "Actor",
Self::Role => "Role",
Self::Activity => "Activity",
Self::Capability => "Capability",
Self::OperationalMission => "Operational mission",
Self::Process => "Process",
Self::System => "System",
Self::SystemActor => "System actor",
Self::Function => "Function",
Self::SystemCapability => "System capability",
Self::Mission => "Mission",
Self::FunctionalChain => "Functional chain",
Self::ExchangeItem => "Exchange item",
Self::ExchangeCategory => "Exchange category",
Self::DataType => "Data type",
Self::DataField => "Data field",
Self::Interface => "Interface",
Self::Port => "Port",
Self::Scenario => "Scenario",
Self::Lifeline => "Lifeline",
Self::Occurrence => "Occurrence",
Self::StateMachine => "State machine",
Self::Region => "Region",
Self::State => "State",
Self::Mode => "Mode",
Self::Initial => "Initial",
Self::Final => "Final",
Self::Choice => "Choice",
Self::Junction => "Junction",
Self::Fork => "Fork",
Self::Join => "Join",
Self::ShallowHistory => "Shallow history",
Self::DeepHistory => "Deep history",
Self::EntryPoint => "Entry point",
Self::ExitPoint => "Exit point",
Self::Terminate => "Terminate",
Self::Event => "Event",
Self::Constraint => "Constraint",
}
}
pub fn is_performer(self) -> bool {
matches!(
self,
Self::Entity | Self::Actor | Self::System | Self::SystemActor
)
}
pub fn is_work(self) -> bool {
matches!(self, Self::Activity | Self::Function)
}
pub fn is_sequence(self) -> bool {
matches!(self, Self::Process | Self::FunctionalChain | Self::Scenario)
}
pub fn is_state(self) -> bool {
self.state_kind().is_some()
}
pub fn state_kind(self) -> Option<StateKind> {
Some(match self {
Self::State => StateKind::State,
Self::Mode => StateKind::Mode,
Self::Initial => StateKind::Initial,
Self::Final => StateKind::Final,
Self::Choice => StateKind::Choice,
Self::Junction => StateKind::Junction,
Self::Fork => StateKind::Fork,
Self::Join => StateKind::Join,
Self::ShallowHistory => StateKind::ShallowHistory,
Self::DeepHistory => StateKind::DeepHistory,
Self::EntryPoint => StateKind::EntryPoint,
Self::ExitPoint => StateKind::ExitPoint,
Self::Terminate => StateKind::Terminate,
_ => return None,
})
}
pub fn is_stable_state(self) -> bool {
matches!(self, Self::State | Self::Mode)
}
pub fn supports(self, layer: Layer) -> bool {
match self {
Self::Entity
| Self::Actor
| Self::Role
| Self::Activity
| Self::Capability
| Self::OperationalMission
| Self::Process => layer == Layer::Oa,
Self::System
| Self::SystemActor
| Self::Function
| Self::SystemCapability
| Self::Mission
| Self::FunctionalChain => layer == Layer::Sa,
_ => true,
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Size {
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Element {
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
pub parent: Option<Id>,
pub body: ElementBody,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ElementBody {
Oa(OaBody),
Sa(SaBody),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum OaBody {
Entity,
Actor,
Role,
Activity,
Capability,
Mission,
Process { steps: Vec<Id> },
Shared(SharedBody),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SaBody {
System,
Actor,
Function,
Capability,
Mission,
FunctionalChain { steps: Vec<Id> },
Shared(SharedBody),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExchangeItemKind {
Information,
Material,
Energy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ExchangeMechanism {
#[default]
Unspecified,
Flow,
Event,
Operation,
SharedData,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ParameterDirection {
#[default]
Unspecified,
In,
Out,
InOut,
Return,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FieldStorage {
#[default]
Value,
Reference,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum DataValue {
Boolean(bool),
Integer(i64),
Real(f64),
String(String),
Enumeration(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TransmissionProtocol {
#[default]
Unspecified,
Synchronous,
Asynchronous,
Unicast,
Multicast,
Broadcast,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AcquisitionProtocol {
#[default]
Unspecified,
Read,
Accept,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum DataTypeDefinition {
Boolean,
Integer {
minimum: Option<i64>,
maximum: Option<i64>,
},
Real {
minimum: Option<f64>,
maximum: Option<f64>,
},
String {
minimum_length: u32,
maximum_length: Option<u32>,
},
Enumeration {
literals: Vec<String>,
},
Record {
supertype: Option<Id>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum TypeRef {
Boolean,
Integer,
Real,
String,
Named(Id),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PortDirection {
In,
Out,
InOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentPortKind {
Standard,
Flow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentPortOrientation {
Unset,
In,
Out,
InOut,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LegacyPortContract {
pub direction: PortDirection,
pub interface: Option<Id>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum PortContract {
FunctionInput {
incoming_items: Vec<Id>,
},
FunctionOutput {
outgoing_items: Vec<Id>,
},
Component {
kind: ComponentPortKind,
orientation: ComponentPortOrientation,
},
Legacy,
}
impl PortContract {
pub fn items(&self) -> &[Id] {
match self {
Self::FunctionInput { incoming_items } => incoming_items,
Self::FunctionOutput { outgoing_items } => outgoing_items,
_ => &[],
}
}
pub fn is_function(&self) -> bool {
matches!(
self,
Self::FunctionInput { .. } | Self::FunctionOutput { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StateKind {
State,
Mode,
Initial,
Final,
Choice,
Junction,
Fork,
Join,
ShallowHistory,
DeepHistory,
EntryPoint,
ExitPoint,
Terminate,
}
impl StateKind {
pub fn kind(self) -> Kind {
match self {
Self::State => Kind::State,
Self::Mode => Kind::Mode,
Self::Initial => Kind::Initial,
Self::Final => Kind::Final,
Self::Choice => Kind::Choice,
Self::Junction => Kind::Junction,
Self::Fork => Kind::Fork,
Self::Join => Kind::Join,
Self::ShallowHistory => Kind::ShallowHistory,
Self::DeepHistory => Kind::DeepHistory,
Self::EntryPoint => Kind::EntryPoint,
Self::ExitPoint => Kind::ExitPoint,
Self::Terminate => Kind::Terminate,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TimeEventKind {
After,
At,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum EventBody {
Signal {
items: Vec<Id>,
},
Time {
kind: TimeEventKind,
expression: String,
},
Change {
expression: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TransitionKind {
#[default]
External,
Local,
Internal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StateActionKind {
Entry,
Do,
Exit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FragmentOperator {
Alternative,
Parallel,
Loop,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControlKind {
Choice,
Merge,
Fork,
Join,
Loop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ScenarioViewpoint {
OperationalActivity,
ParticipantInteraction,
Functional,
BoundaryInterface,
#[default]
MixedLegacy,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub enum MessageKind {
#[default]
Asynchronous,
Call,
Reply,
Create,
Delete,
Timer {
expression: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LifelineBinding {
pub referenced: Id,
pub local: Id,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageValue {
#[serde(default)]
pub path: Vec<Id>,
pub field: Id,
pub expression: String,
}
/// Explicit operation intent. A missing allocation remains an unfinished operation,
/// whereas an absent OperationUse retains ordinary channel-message semantics.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperationUse {
pub allocation: Option<Id>,
}
/// A contribution in a caller instance. Paths contain reference occurrence IDs,
/// never sequence definitions; repeated references therefore remain distinct.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OccurrenceEndpoint {
pub references: Vec<Id>,
pub occurrence: Id,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ContextualLinkPurpose {
Precedence { guard: String },
Exchange { exchange: Id },
CommonWork { work: Id },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum OccurrenceBody {
Exchange {
exchange: Option<Id>,
sender: Option<Id>,
receiver: Option<Id>,
#[serde(default)]
message_kind: MessageKind,
#[serde(default)]
reply_to: Option<Id>,
#[serde(default)]
values: Vec<MessageValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
operation: Option<OperationUse>,
},
Action {
action: Option<Id>,
lifeline: Option<Id>,
},
Control {
kind: ControlKind,
},
SequenceReference {
sequence: Option<Id>,
},
ScenarioUse {
scenario: Option<Id>,
bindings: Vec<LifelineBinding>,
},
State {
state: Option<Id>,
lifeline: Option<Id>,
},
Fragment {
operator: FragmentOperator,
branches: Vec<Id>,
},
Branch {
guard: String,
occurrences: Vec<Id>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SharedBody {
ExchangeCategory {
family: crate::view_options::ExchangeFamily,
exchanges: Vec<Id>,
},
ExchangeItem {
kind: ExchangeItemKind,
mechanism: ExchangeMechanism,
supertype: Option<Id>,
},
DataType {
definition: DataTypeDefinition,
default: Option<DataValue>,
unit: String,
},
DataField {
type_ref: TypeRef,
minimum: u32,
maximum: Option<u32>,
unit: String,
direction: ParameterDirection,
storage: FieldStorage,
default: Option<Vec<DataValue>>,
},
Interface {
supertype: Option<Id>,
},
Port {
contract: PortContract,
legacy: Option<LegacyPortContract>,
},
Scenario {
#[serde(default)]
viewpoint: ScenarioViewpoint,
context: Option<Id>,
precondition: String,
postcondition: String,
steps: Vec<Id>,
},
Lifeline {
represented: Option<Id>,
},
Occurrence {
body: OccurrenceBody,
precondition: String,
postcondition: String,
},
StateMachine,
Event {
body: EventBody,
},
Region,
State {
kind: StateKind,
entry: String,
exit: String,
do_activity: String,
},
Constraint {
expression: String,
constrained: Vec<Id>,
},
}
impl Element {
pub fn layer(&self) -> Layer {
self.body.layer()
}
pub fn kind(&self) -> Kind {
self.body.kind()
}
pub fn shared(&self) -> Option<&SharedBody> {
self.body.shared()
}
pub fn shared_mut(&mut self) -> Option<&mut SharedBody> {
self.body.shared_mut()
}
pub fn steps(&self) -> Option<&Vec<Id>> {
self.body.steps()
}
pub fn steps_mut(&mut self) -> Option<&mut Vec<Id>> {
self.body.steps_mut()
}
}
impl ElementBody {
pub fn layer(&self) -> Layer {
match self {
Self::Oa(_) => Layer::Oa,
Self::Sa(_) => Layer::Sa,
}
}
pub fn shared(&self) -> Option<&SharedBody> {
match self {
Self::Oa(OaBody::Shared(v)) | Self::Sa(SaBody::Shared(v)) => Some(v),
_ => None,
}
}
pub fn shared_mut(&mut self) -> Option<&mut SharedBody> {
match self {
Self::Oa(OaBody::Shared(v)) | Self::Sa(SaBody::Shared(v)) => Some(v),
_ => None,
}
}
pub fn steps(&self) -> Option<&Vec<Id>> {
match self {
Self::Oa(OaBody::Process { steps }) | Self::Sa(SaBody::FunctionalChain { steps }) => {
Some(steps)
}
_ => match self.shared() {
Some(SharedBody::Scenario { steps, .. }) => Some(steps),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Branch { occurrences, .. },
..
}) => Some(occurrences),
_ => None,
},
}
}
pub fn steps_mut(&mut self) -> Option<&mut Vec<Id>> {
match self {
Self::Oa(OaBody::Process { steps }) | Self::Sa(SaBody::FunctionalChain { steps }) => {
Some(steps)
}
_ => match self.shared_mut() {
Some(SharedBody::Scenario { steps, .. }) => Some(steps),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Branch { occurrences, .. },
..
}) => Some(occurrences),
_ => None,
},
}
}
pub fn kind(&self) -> Kind {
match self {
Self::Oa(v) => match v {
OaBody::Entity => Kind::Entity,
OaBody::Actor => Kind::Actor,
OaBody::Role => Kind::Role,
OaBody::Activity => Kind::Activity,
OaBody::Capability => Kind::Capability,
OaBody::Mission => Kind::OperationalMission,
OaBody::Process { .. } => Kind::Process,
OaBody::Shared(s) => s.kind(),
},
Self::Sa(v) => match v {
SaBody::System => Kind::System,
SaBody::Actor => Kind::SystemActor,
SaBody::Function => Kind::Function,
SaBody::Capability => Kind::SystemCapability,
SaBody::Mission => Kind::Mission,
SaBody::FunctionalChain { .. } => Kind::FunctionalChain,
SaBody::Shared(s) => s.kind(),
},
}
}
pub fn shared_in(layer: Layer, body: SharedBody) -> Self {
match layer {
Layer::Oa => Self::Oa(OaBody::Shared(body)),
Layer::Sa => Self::Sa(SaBody::Shared(body)),
}
}
pub fn new(kind: Kind, layer: Layer) -> Option<Self> {
if !kind.supports(layer) {
return None;
}
Some(match kind {
Kind::Entity => Self::Oa(OaBody::Entity),
Kind::Actor => Self::Oa(OaBody::Actor),
Kind::Role => Self::Oa(OaBody::Role),
Kind::Activity => Self::Oa(OaBody::Activity),
Kind::Capability => Self::Oa(OaBody::Capability),
Kind::OperationalMission => Self::Oa(OaBody::Mission),
Kind::Process => Self::Oa(OaBody::Process { steps: vec![] }),
Kind::System => Self::Sa(SaBody::System),
Kind::SystemActor => Self::Sa(SaBody::Actor),
Kind::Function => Self::Sa(SaBody::Function),
Kind::SystemCapability => Self::Sa(SaBody::Capability),
Kind::Mission => Self::Sa(SaBody::Mission),
Kind::FunctionalChain => Self::Sa(SaBody::FunctionalChain { steps: vec![] }),
k => Self::shared_in(
layer,
match k {
Kind::ExchangeItem => SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Unspecified,
supertype: None,
},
Kind::DataType => SharedBody::DataType {
definition: DataTypeDefinition::String {
minimum_length: 0,
maximum_length: None,
},
default: None,
unit: String::new(),
},
Kind::ExchangeCategory => SharedBody::ExchangeCategory {
family: crate::view_options::ExchangeFamily::Work,
exchanges: vec![],
},
Kind::DataField => SharedBody::DataField {
type_ref: TypeRef::String,
minimum: 1,
maximum: Some(1),
unit: String::new(),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
default: None,
},
Kind::Interface => SharedBody::Interface { supertype: None },
// Port creation requires an explicit contract and owner.
Kind::Port => return None,
Kind::Scenario => SharedBody::Scenario {
viewpoint: if layer == Layer::Oa {
ScenarioViewpoint::OperationalActivity
} else {
ScenarioViewpoint::Functional
},
context: None,
precondition: String::new(),
postcondition: String::new(),
steps: vec![],
},
Kind::Lifeline => SharedBody::Lifeline { represented: None },
Kind::Occurrence => SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: None,
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![],
operation: None,
},
precondition: String::new(),
postcondition: String::new(),
},
Kind::StateMachine => SharedBody::StateMachine,
Kind::Event => SharedBody::Event {
body: EventBody::Signal { items: vec![] },
},
Kind::Region => SharedBody::Region,
k if k.is_state() => SharedBody::State {
kind: k.state_kind().expect("state kind"),
entry: String::new(),
exit: String::new(),
do_activity: String::new(),
},
Kind::Constraint => SharedBody::Constraint {
expression: String::new(),
constrained: vec![],
},
_ => unreachable!(),
},
),
})
}
}
impl SharedBody {
pub fn kind(&self) -> Kind {
match self {
Self::ExchangeItem { .. } => Kind::ExchangeItem,
Self::ExchangeCategory { .. } => Kind::ExchangeCategory,
Self::DataType { .. } => Kind::DataType,
Self::DataField { .. } => Kind::DataField,
Self::Interface { .. } => Kind::Interface,
Self::Port { .. } => Kind::Port,
Self::Scenario { .. } => Kind::Scenario,
Self::Lifeline { .. } => Kind::Lifeline,
Self::Occurrence { .. } => Kind::Occurrence,
Self::StateMachine => Kind::StateMachine,
Self::Event { .. } => Kind::Event,
Self::Region => Kind::Region,
Self::State { kind, .. } => kind.kind(),
Self::Constraint { .. } => Kind::Constraint,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RelationKind {
InterfaceItemAllocation,
DataAssociation,
Allocation,
PortAllocation,
Interaction,
CapabilityInvolvement,
ChainInvolvement,
RoleRealization,
RoleAssignment,
FunctionalExchange,
BoundaryExchange,
CommunicationMean,
MissionCapability,
CapabilityInclude,
CapabilityExtend,
CapabilityGeneralization,
MissionInvolvement,
SequenceLink,
ContextualLink,
StateTransition,
Availability,
ExchangeAllocation,
InterfaceProvision,
InterfaceRequirement,
StateAction,
}
impl RelationKind {
pub const ALL: [Self; 25] = [
Self::InterfaceItemAllocation,
Self::DataAssociation,
Self::Allocation,
Self::PortAllocation,
Self::Interaction,
Self::CapabilityInvolvement,
Self::ChainInvolvement,
Self::RoleRealization,
Self::RoleAssignment,
Self::FunctionalExchange,
Self::BoundaryExchange,
Self::CommunicationMean,
Self::MissionCapability,
Self::CapabilityInclude,
Self::CapabilityExtend,
Self::CapabilityGeneralization,
Self::MissionInvolvement,
Self::SequenceLink,
Self::ContextualLink,
Self::StateTransition,
Self::Availability,
Self::ExchangeAllocation,
Self::InterfaceProvision,
Self::InterfaceRequirement,
Self::StateAction,
];
pub fn permits(self, from: Kind, to: Kind) -> bool {
match self {
Self::InterfaceItemAllocation => from == Kind::Interface && to == Kind::ExchangeItem,
Self::DataAssociation => from == Kind::DataType && to == Kind::DataType,
Self::Allocation => from.is_performer() && to.is_work(),
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
Self::CapabilityInvolvement => {
(from.is_performer() || from.is_work() || from == Kind::Role)
&& matches!(to, Kind::Capability | Kind::SystemCapability)
}
Self::ChainInvolvement => {
matches!(from, Kind::Process | Kind::FunctionalChain)
&& matches!(to, Kind::Capability | Kind::SystemCapability)
}
Self::RoleRealization => matches!(from, Kind::Entity | Kind::Actor) && to == Kind::Role,
Self::RoleAssignment => from == Kind::Role && to == Kind::Activity,
Self::FunctionalExchange => from == Kind::Function && to == Kind::Function,
Self::BoundaryExchange => from == Kind::Port && to == Kind::Port,
Self::CommunicationMean => {
matches!(from, Kind::Entity | Kind::Actor)
&& matches!(to, Kind::Entity | Kind::Actor)
}
Self::MissionCapability => matches!(
(from, to),
(Kind::OperationalMission, Kind::Capability)
| (Kind::Mission, Kind::SystemCapability)
),
Self::CapabilityInclude | Self::CapabilityExtend | Self::CapabilityGeneralization => {
matches!(
(from, to),
(Kind::Capability, Kind::Capability)
| (Kind::SystemCapability, Kind::SystemCapability)
)
}
Self::MissionInvolvement => matches!(
(from, to),
(Kind::Entity | Kind::Actor, Kind::OperationalMission)
| (Kind::System | Kind::SystemActor, Kind::Mission)
),
Self::SequenceLink | Self::ContextualLink => {
from == Kind::Occurrence && to == Kind::Occurrence
}
Self::StateTransition => from.is_state() && to.is_state(),
Self::ExchangeAllocation => false,
Self::InterfaceProvision | Self::InterfaceRequirement => {
(from.is_performer() || from == Kind::Port) && to == Kind::Interface
}
Self::StateAction => from.is_stable_state() && to.is_work(),
Self::Availability => {
(from.is_work()
|| from.is_performer()
|| matches!(
from,
Kind::Capability
| Kind::SystemCapability
| Kind::Process
| Kind::FunctionalChain
))
&& to.is_stable_state()
}
}
}
pub fn hint(self) -> &'static str {
match self {
Self::InterfaceItemAllocation => "Interface → allocated exchange item",
Self::DataAssociation => "Record type → record type",
Self::Allocation => "Performer → leaf activity/function",
Self::PortAllocation => "Component port → allocated function port",
Self::Interaction => "OA activity → OA activity",
Self::CapabilityInvolvement => "Participant/activity/function → capability",
Self::ChainInvolvement => "Process/chain → capability",
Self::RoleRealization => "OA entity/actor → role",
Self::RoleAssignment => "OA role → activity",
Self::FunctionalExchange => "SA function → SA function",
Self::BoundaryExchange => "Output port → input port",
Self::CommunicationMean => "OA entity/actor → OA entity/actor",
Self::MissionCapability => "Mission → capability in the same perspective",
Self::CapabilityInclude => "Including capability → included capability",
Self::CapabilityExtend => {
"Extending capability → extended capability, under a condition"
}
Self::CapabilityGeneralization => "Specific capability → general capability",
Self::MissionInvolvement => "Participant → mission in the same perspective",
Self::SequenceLink => "Preceding → following contribution in the same process or chain",
Self::ContextualLink => {
"Contributions addressed in an explicit process or chain caller"
}
Self::StateTransition => "State → state in one machine",
Self::ExchangeAllocation => {
"Communication means / boundary exchange → operational interaction / functional exchange"
}
Self::InterfaceProvision => "Participant/port → provided interface",
Self::InterfaceRequirement => "Participant/port → required interface",
Self::StateAction => "State/mode → activity/function",
Self::Availability => "Behavior/participant → state/mode",
}
}
}
impl fmt::Display for RelationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InterfaceItemAllocation => "Interface item allocation",
Self::DataAssociation => "Data association",
Self::Allocation => "Allocation",
Self::PortAllocation => "Port allocation",
Self::Interaction => "Interaction",
Self::CapabilityInvolvement => "Capability involvement",
Self::ChainInvolvement => "Chain involvement",
Self::RoleRealization => "Role realization",
Self::RoleAssignment => "Role assignment",
Self::FunctionalExchange => "Functional exchange",
Self::BoundaryExchange => "Boundary exchange",
Self::CommunicationMean => "Communication means",
Self::MissionCapability => "Mission capability",
Self::CapabilityInclude => "Capability include",
Self::CapabilityExtend => "Capability extend",
Self::CapabilityGeneralization => "Capability generalization",
Self::MissionInvolvement => "Mission involvement",
Self::SequenceLink => "Sequence link",
Self::ContextualLink => "Contextual link",
Self::StateTransition => "State transition",
Self::ExchangeAllocation => "Exchange allocation",
Self::InterfaceProvision => "Interface provision",
Self::InterfaceRequirement => "Interface requirement",
Self::StateAction => "State action",
Self::Availability => "Availability",
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Relation {
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
pub body: RelationBody,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum RelationBody {
InterfaceItemAllocation {
source: Id,
target: Id,
order: u32,
transmission: TransmissionProtocol,
acquisition: AcquisitionProtocol,
},
DataAssociation {
source: Id,
target: Id,
source_role: String,
target_role: String,
source_minimum: u32,
source_maximum: Option<u32>,
target_minimum: u32,
target_maximum: Option<u32>,
source_navigable: bool,
target_navigable: bool,
},
Allocation {
source: Id,
target: Id,
},
PortAllocation {
source: Id,
target: Id,
},
Interaction {
source: Id,
target: Id,
items: Vec<Id>,
},
CapabilityInvolvement {
source: Id,
target: Id,
},
ChainInvolvement {
source: Id,
target: Id,
},
RoleRealization {
source: Id,
target: Id,
},
RoleAssignment {
source: Id,
target: Id,
},
FunctionalExchange {
source: Id,
target: Id,
source_port: Id,
target_port: Id,
items: Vec<Id>,
},
BoundaryExchange {
source: Id,
target: Id,
oriented: bool,
items: Vec<Id>,
},
CommunicationMean {
source: Id,
target: Id,
oriented: bool,
items: Vec<Id>,
},
MissionCapability {
source: Id,
target: Id,
},
CapabilityInclude {
source: Id,
target: Id,
},
CapabilityExtend {
source: Id,
target: Id,
condition: String,
},
CapabilityGeneralization {
source: Id,
target: Id,
},
MissionInvolvement {
source: Id,
target: Id,
},
SequenceLink {
source: Id,
target: Id,
guard: String,
},
ContextualLink {
owner: Id,
source: OccurrenceEndpoint,
target: OccurrenceEndpoint,
purpose: ContextualLinkPurpose,
},
StateTransition {
source: Id,
target: Id,
#[serde(default)]
transition_kind: TransitionKind,
#[serde(default)]
triggers: Vec<Id>,
#[serde(default)]
effects: Vec<Id>,
trigger: String,
guard: String,
effect: String,
},
Availability {
source: Id,
target: Id,
},
ExchangeAllocation {
source: Id,
target: Id,
},
InterfaceProvision {
source: Id,
target: Id,
},
InterfaceRequirement {
source: Id,
target: Id,
},
StateAction {
source: Id,
target: Id,
phase: StateActionKind,
},
}
impl Relation {
/// Caller ownership cannot be inferred from a contextual link's leaf IDs.
pub fn contextual_owner(&self) -> Option<Id> {
match self.body {
RelationBody::ContextualLink { owner, .. } => Some(owner),
_ => None,
}
}
pub fn kind(&self) -> RelationKind {
self.body.kind()
}
pub fn source(&self) -> Id {
self.body.endpoints().0
}
pub fn target(&self) -> Id {
self.body.endpoints().1
}
}
impl RelationBody {
pub fn kind(&self) -> RelationKind {
match self {
Self::InterfaceItemAllocation { .. } => RelationKind::InterfaceItemAllocation,
Self::DataAssociation { .. } => RelationKind::DataAssociation,
Self::Allocation { .. } => RelationKind::Allocation,
Self::PortAllocation { .. } => RelationKind::PortAllocation,
Self::Interaction { .. } => RelationKind::Interaction,
Self::CapabilityInvolvement { .. } => RelationKind::CapabilityInvolvement,
Self::ChainInvolvement { .. } => RelationKind::ChainInvolvement,
Self::RoleRealization { .. } => RelationKind::RoleRealization,
Self::RoleAssignment { .. } => RelationKind::RoleAssignment,
Self::FunctionalExchange { .. } => RelationKind::FunctionalExchange,
Self::BoundaryExchange { .. } => RelationKind::BoundaryExchange,
Self::CommunicationMean { .. } => RelationKind::CommunicationMean,
Self::MissionCapability { .. } => RelationKind::MissionCapability,
Self::CapabilityInclude { .. } => RelationKind::CapabilityInclude,
Self::CapabilityExtend { .. } => RelationKind::CapabilityExtend,
Self::CapabilityGeneralization { .. } => RelationKind::CapabilityGeneralization,
Self::MissionInvolvement { .. } => RelationKind::MissionInvolvement,
Self::SequenceLink { .. } => RelationKind::SequenceLink,
Self::ContextualLink { .. } => RelationKind::ContextualLink,
Self::StateTransition { .. } => RelationKind::StateTransition,
Self::ExchangeAllocation { .. } => RelationKind::ExchangeAllocation,
Self::InterfaceProvision { .. } => RelationKind::InterfaceProvision,
Self::InterfaceRequirement { .. } => RelationKind::InterfaceRequirement,
Self::StateAction { .. } => RelationKind::StateAction,
Self::Availability { .. } => RelationKind::Availability,
}
}
pub fn endpoints(&self) -> (Id, Id) {
match self {
Self::ContextualLink { source, target, .. } => (source.occurrence, target.occurrence),
Self::InterfaceItemAllocation { source, target, .. }
| Self::DataAssociation { source, target, .. }
| Self::Allocation { source, target }
| Self::PortAllocation { source, target }
| Self::Interaction { source, target, .. }
| Self::CapabilityInvolvement { source, target }
| Self::ChainInvolvement { source, target }
| Self::RoleRealization { source, target }
| Self::RoleAssignment { source, target }
| Self::FunctionalExchange { source, target, .. }
| Self::BoundaryExchange { source, target, .. }
| Self::CommunicationMean { source, target, .. }
| Self::MissionCapability { source, target }
| Self::CapabilityInclude { source, target }
| Self::CapabilityExtend { source, target, .. }
| Self::CapabilityGeneralization { source, target }
| Self::MissionInvolvement { source, target }
| Self::SequenceLink { source, target, .. }
| Self::StateTransition { source, target, .. }
| Self::ExchangeAllocation { source, target }
| Self::InterfaceProvision { source, target }
| Self::InterfaceRequirement { source, target }
| Self::StateAction { source, target, .. }
| Self::Availability { source, target } => (*source, *target),
}
}
pub fn set_endpoints(&mut self, a: Id, b: Id) {
match self {
Self::ContextualLink { source, target, .. } => {
source.occurrence = a;
target.occurrence = b;
}
Self::InterfaceItemAllocation { source, target, .. }
| Self::DataAssociation { source, target, .. }
| Self::Allocation { source, target }
| Self::PortAllocation { source, target }
| Self::Interaction { source, target, .. }
| Self::CapabilityInvolvement { source, target }
| Self::ChainInvolvement { source, target }
| Self::RoleRealization { source, target }
| Self::RoleAssignment { source, target }
| Self::FunctionalExchange { source, target, .. }
| Self::BoundaryExchange { source, target, .. }
| Self::CommunicationMean { source, target, .. }
| Self::MissionCapability { source, target }
| Self::CapabilityInclude { source, target }
| Self::CapabilityExtend { source, target, .. }
| Self::CapabilityGeneralization { source, target }
| Self::MissionInvolvement { source, target }
| Self::SequenceLink { source, target, .. }
| Self::StateTransition { source, target, .. }
| Self::ExchangeAllocation { source, target }
| Self::InterfaceProvision { source, target }
| Self::InterfaceRequirement { source, target }
| Self::StateAction { source, target, .. }
| Self::Availability { source, target } => {
*source = a;
*target = b;
}
}
}
pub fn items(&self) -> &[Id] {
match self {
Self::Interaction { items, .. }
| Self::FunctionalExchange { items, .. }
| Self::BoundaryExchange { items, .. }
| Self::CommunicationMean { items, .. } => items,
_ => &[],
}
}
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
match kind {
RelationKind::InterfaceItemAllocation => Self::InterfaceItemAllocation {
source,
target,
order: 0,
transmission: TransmissionProtocol::Unspecified,
acquisition: AcquisitionProtocol::Unspecified,
},
RelationKind::DataAssociation => Self::DataAssociation {
source,
target,
source_role: String::new(),
target_role: String::new(),
source_minimum: 0,
source_maximum: None,
target_minimum: 0,
target_maximum: None,
source_navigable: false,
target_navigable: false,
},
RelationKind::Allocation => Self::Allocation { source, target },
RelationKind::PortAllocation => Self::PortAllocation { source, target },
RelationKind::Interaction => Self::Interaction {
source,
target,
items: vec![],
},
RelationKind::CapabilityInvolvement => Self::CapabilityInvolvement { source, target },
RelationKind::ChainInvolvement => Self::ChainInvolvement { source, target },
RelationKind::RoleRealization => Self::RoleRealization { source, target },
RelationKind::RoleAssignment => Self::RoleAssignment { source, target },
RelationKind::FunctionalExchange => Self::FunctionalExchange {
source,
target,
// Unbound drafts must be completed before candidate admission.
source_port: Id::nil(),
target_port: Id::nil(),
items: vec![],
},
RelationKind::BoundaryExchange => Self::BoundaryExchange {
source,
target,
oriented: true,
items: vec![],
},
RelationKind::CommunicationMean => Self::CommunicationMean {
source,
target,
oriented: false,
items: vec![],
},
RelationKind::MissionCapability => Self::MissionCapability { source, target },
RelationKind::CapabilityInclude => Self::CapabilityInclude { source, target },
RelationKind::CapabilityExtend => Self::CapabilityExtend {
source,
target,
condition: String::new(),
},
RelationKind::CapabilityGeneralization => {
Self::CapabilityGeneralization { source, target }
}
RelationKind::MissionInvolvement => Self::MissionInvolvement { source, target },
RelationKind::SequenceLink => Self::SequenceLink {
source,
target,
guard: String::new(),
},
RelationKind::ContextualLink => Self::ContextualLink {
owner: Id::nil(),
source: OccurrenceEndpoint {
references: vec![],
occurrence: source,
},
target: OccurrenceEndpoint {
references: vec![],
occurrence: target,
},
purpose: ContextualLinkPurpose::Precedence {
guard: String::new(),
},
},
RelationKind::StateTransition => Self::StateTransition {
source,
target,
transition_kind: TransitionKind::External,
triggers: vec![],
effects: vec![],
trigger: String::new(),
guard: String::new(),
effect: String::new(),
},
RelationKind::ExchangeAllocation => Self::ExchangeAllocation { source, target },
RelationKind::InterfaceProvision => Self::InterfaceProvision { source, target },
RelationKind::InterfaceRequirement => Self::InterfaceRequirement { source, target },
RelationKind::StateAction => Self::StateAction {
source,
target,
phase: StateActionKind::Do,
},
RelationKind::Availability => Self::Availability { source, target },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ProjectionKind {
Overview,
Decomposition,
Allocation,
Traceability,
FunctionalDataflow,
Capabilities,
StateMachine { machine: Id },
}
impl ProjectionKind {
pub fn state_machine(self) -> Option<Id> {
match self {
Self::StateMachine { machine } => Some(machine),
_ => None,
}
}
pub fn includes(self, kind: Kind) -> bool {
match self {
Self::StateMachine { .. } => {
matches!(kind, Kind::StateMachine | Kind::Region) || kind.is_state()
}
Self::Allocation => kind.is_performer() || kind.is_work(),
Self::FunctionalDataflow => kind.is_work(),
Self::Capabilities => {
kind.is_performer()
|| matches!(
kind,
Kind::Role
| Kind::Capability
| Kind::OperationalMission
| Kind::Mission
| Kind::SystemCapability
)
}
_ => true,
}
}
}
impl std::fmt::Display for ProjectionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::StateMachine { .. } => "State machine",
Self::Allocation => "Architecture · allocated work",
Self::FunctionalDataflow => "Functional dataflow",
Self::Capabilities => "Missions & capabilities",
Self::Decomposition => "Containment",
Self::Overview => "All relationships",
Self::Traceability => "OA ↔ SA traceability",
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Diagram {
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
pub layer: Layer,
pub projection: ProjectionKind,
#[serde(default)]
pub view_options: crate::view_options::ViewOptions,
#[serde(deserialize_with = "unique_id_map")]
pub nodes: BTreeMap<Id, NodePlacement>,
#[serde(default)]
pub routes: Vec<EdgeRoute>,
}
/// Authored presentation for one existing relation or trace in one diagram.
/// Computed endpoints and automatic routing elbows are never persisted here.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EdgeRoute {
pub edge: Id,
#[serde(default)]
pub waypoints: Vec<Position>,
#[serde(default)]
pub label: Option<EdgeLabel>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EdgeLabel {
/// Fraction of the computed polyline's total arc length, from source to target.
pub fraction: f32,
/// Diagram-space displacement relative to the point at `fraction`.
pub offset: Position,
}
pub const MAX_ROUTE_POINTS: usize = 128;
/// Presentation side; independent of the semantic port direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum PortAnchorSide {
Left,
Right,
Top,
Bottom,
}
impl PortAnchorSide {
pub fn normal(self) -> Position {
match self {
Self::Left => Position { x: -1., y: 0. },
Self::Right => Position { x: 1., y: 0. },
Self::Top => Position { x: 0., y: -1. },
Self::Bottom => Position { x: 0., y: 1. },
}
}
}
/// Manual position of a semantic child port on one owner placement.
/// Absence from the placement's list selects automatic positioning.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortAnchor {
pub port: Id,
pub side: PortAnchorSide,
/// Fraction of the complete edge, from top (vertical) or left (horizontal).
pub offset: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodePlacement {
pub id: Id,
pub alias: String,
pub element: Id,
pub position: Position,
pub size: Size,
pub collapsed: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub port_anchors: Vec<PortAnchor>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ArtifactRelation {
References,
Satisfies,
Verifies,
Implements,
DerivedFrom,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ArtifactLink {
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
pub owner: Id,
pub relation: ArtifactRelation,
pub uri: String,
pub revision: Option<String>,
pub fragment: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TraceKind {
Refinement,
Participant,
Capability,
Mission,
Exchange,
Sequence,
Item,
Scenario,
State,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TracedProperties {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TraceBaseline {
pub oa: TracedProperties,
pub sa: TracedProperties,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semantic: Option<crate::semantic::SemanticBaseline>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Trace {
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
pub oa: Id,
pub sa: Id,
pub kind: TraceKind,
pub baseline: Option<TraceBaseline>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Project {
pub schema_version: u32,
pub id: Id,
pub alias: String,
pub name: String,
pub description: String,
#[serde(deserialize_with = "unique_id_map")]
pub elements: BTreeMap<Id, Element>,
#[serde(deserialize_with = "unique_id_map")]
pub relations: BTreeMap<Id, Relation>,
#[serde(deserialize_with = "unique_id_map")]
pub diagrams: BTreeMap<Id, Diagram>,
#[serde(deserialize_with = "unique_id_map")]
pub artifact_links: BTreeMap<Id, ArtifactLink>,
#[serde(deserialize_with = "unique_id_map")]
pub traces: BTreeMap<Id, Trace>,
}
pub fn unique_id_map<'de, D, T>(deserializer: D) -> Result<BTreeMap<Id, T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
struct Unique<T>(std::marker::PhantomData<T>);
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Unique<T> {
type Value = BTreeMap<Id, T>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("unique UUID keys")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut access: A,
) -> Result<Self::Value, A::Error> {
let mut map = BTreeMap::new();
while let Some((id, value)) = access.next_entry()? {
if map.insert(id, value).is_some() {
return Err(serde::de::Error::custom(format!(
"Duplicate object ID {id}"
)));
}
}
Ok(map)
}
}
deserializer.deserialize_map(Unique(std::marker::PhantomData))
}
pub fn reserved_alias(value: &str) -> bool {
matches!(
value,
"arcadia"
| "project"
| "id"
| "oa"
| "sa"
| "element"
| "relation"
| "diagram"
| "node"
| "artifact"
| "trace"
| "body"
| "alias"
| "name"
| "description"
| "parent"
| "from"
| "to"
| "entity"
| "actor"
| "activity"
| "capability"
| "process"
| "allocation"
| "interaction"
| "contribution"
| "realization"
| "position"
| "steps"
| "true"
| "false"
| "null"
)
}
pub fn valid_alias(value: &str) -> bool {
let mut chars = value.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !reserved_alias(value)
}
impl Project {
pub fn empty() -> Self {
Self {
schema_version: SCHEMA_VERSION,
id: Id::new_v4(),
alias: "mission".into(),
name: "Untitled mission".into(),
description: String::new(),
elements: BTreeMap::new(),
relations: BTreeMap::new(),
diagrams: BTreeMap::new(),
artifact_links: BTreeMap::new(),
traces: BTreeMap::new(),
}
}
pub fn identities(&self) -> Vec<(Id, &str)> {
let mut out = vec![(self.id, self.alias.as_str())];
out.extend(self.elements.values().map(|v| (v.id, v.alias.as_str())));
out.extend(self.relations.values().map(|v| (v.id, v.alias.as_str())));
out.extend(self.diagrams.values().map(|v| (v.id, v.alias.as_str())));
out.extend(
self.diagrams
.values()
.flat_map(|v| v.nodes.values())
.map(|v| (v.id, v.alias.as_str())),
);
out.extend(
self.artifact_links
.values()
.map(|v| (v.id, v.alias.as_str())),
);
out.extend(self.traces.values().map(|v| (v.id, v.alias.as_str())));
out
}
pub fn contains(&self, id: Id) -> bool {
self.identities().iter().any(|(i, _)| *i == id)
}
pub fn fresh_alias(&self, name: &str, fallback: &str, id: Id) -> String {
let used: BTreeSet<_> = self.identities().into_iter().map(|(_, a)| a).collect();
let mut stem = String::new();
for c in name.chars() {
if c.is_ascii_alphanumeric() {
stem.push(c.to_ascii_lowercase())
} else if !stem.is_empty() && !stem.ends_with('_') {
stem.push('_')
}
}
let mut stem = stem.trim_end_matches('_').to_owned();
if !valid_alias(&stem) {
stem = format!("{fallback}_{}", stem.trim_matches('_'));
}
if !valid_alias(&stem) {
stem = "item".into();
}
if !used.contains(stem.as_str()) {
return stem;
}
let candidate = format!("{stem}_{}", &id.simple().to_string()[..8]);
if !used.contains(candidate.as_str()) {
return candidate;
}
let mut n = 2;
loop {
let value = format!("{candidate}_{n}");
if !used.contains(value.as_str()) {
return value;
}
n += 1;
}
}
pub fn named(&self, id: Id) -> &str {
if id == self.id {
return &self.name;
}
if let Some(v) = self.elements.get(&id) {
return &v.name;
}
if let Some(v) = self.relations.get(&id) {
return &v.name;
}
if let Some(v) = self.traces.get(&id) {
return &v.name;
}
if let Some(v) = self.artifact_links.get(&id) {
return &v.name;
}
if let Some(v) = self.diagrams.get(&id) {
return &v.name;
}
"Missing object"
}
pub fn layer_of(&self, id: Id) -> Option<Layer> {
let mut current = id;
let mut seen = BTreeSet::new();
loop {
if !seen.insert(current) {
return None;
}
if let Some(e) = self.elements.get(&current) {
return Some(e.layer());
}
current = self.relations.get(&current)?.source();
}
}
pub fn properties(&self, id: Id) -> Option<TracedProperties> {
if let Some(e) = self.elements.get(&id) {
return Some(TracedProperties {
name: e.name.clone(),
description: e.description.clone(),
});
}
self.relations.get(&id).map(|r| TracedProperties {
name: r.name.clone(),
description: r.description.clone(),
})
}
pub fn children(&self, parent: Id) -> impl Iterator<Item = &Element> {
self.elements
.values()
.filter(move |e| e.parent == Some(parent))
}
pub fn make_element(
&self,
kind: Kind,
layer: Layer,
name: &str,
parent: Option<Id>,
) -> Option<Element> {
let id = Id::new_v4();
let mut body = ElementBody::new(kind, layer)?;
if kind == Kind::Occurrence
&& parent
.and_then(|id| self.elements.get(&id))
.is_some_and(|owner| {
matches!(
owner.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { .. },
..
})
)
})
{
body = ElementBody::shared_in(
layer,
SharedBody::Occurrence {
body: OccurrenceBody::Branch {
guard: String::new(),
occurrences: vec![],
},
precondition: String::new(),
postcondition: String::new(),
},
);
}
Some(Element {
id,
alias: self.fresh_alias(name, "element", id),
name: name.into(),
description: String::new(),
parent,
body,
})
}
pub fn add_element(
&mut self,
kind: Kind,
layer: Layer,
name: &str,
parent: Option<Id>,
) -> Option<Id> {
let e = self.make_element(kind, layer, name, parent)?;
let id = e.id;
self.elements.insert(id, e);
Some(id)
}
pub fn add(&mut self, kind: Kind, name: &str, x: f32, y: f32) -> Id {
let layer = if kind.supports(Layer::Oa) {
Layer::Oa
} else {
Layer::Sa
};
let id = self
.add_element(kind, layer, name, None)
.expect("kind belongs to a layer");
let diagram = self.ensure_diagram(layer);
self.place(diagram, id, Position { x, y });
id
}
pub fn ensure_diagram(&mut self, layer: Layer) -> Id {
if let Some(d) = self
.diagrams
.values()
.find(|d| d.layer == layer && d.projection == ProjectionKind::Overview)
{
return d.id;
}
let id = Id::new_v4();
let name = format!("{layer} overview");
let alias = self.fresh_alias(&name, "diagram", id);
self.diagrams.insert(
id,
Diagram {
id,
alias,
name,
description: String::new(),
layer,
projection: ProjectionKind::Overview,
view_options: crate::view_options::ViewOptions::default(),
nodes: BTreeMap::new(),
routes: vec![],
},
);
id
}
pub fn place(&mut self, diagram: Id, element: Id, position: Position) -> Id {
if let Some(node) = self
.diagrams
.get(&diagram)
.and_then(|d| d.nodes.values().find(|n| n.element == element))
{
return node.id;
}
let id = Id::new_v4();
let alias = self.fresh_alias(&format!("view_{}", self.named(element)), "node", id);
let node = NodePlacement {
id,
alias,
element,
position,
size: Size {
width: 224.,
height: 94.,
},
collapsed: false,
port_anchors: Vec::new(),
};
if let Some(d) = self.diagrams.get_mut(&diagram) {
d.nodes.insert(id, node);
}
id
}
pub fn relate(&mut self, kind: RelationKind, name: &str, source: Id, target: Id) -> Id {
let id = Id::new_v4();
let alias = self.fresh_alias(name, "relation", id);
if kind == RelationKind::FunctionalExchange
&& self.elements.contains_key(&source)
&& self.elements.contains_key(&target)
{
let request = crate::ports::FunctionalExchangeRequest {
id,
alias,
name: name.into(),
description: String::new(),
source,
target,
source_port: crate::ports::PortChoice::Auto,
target_port: crate::ports::PortChoice::Auto,
items: vec![],
};
// Project is the unchecked builder; Session validates the complete batch.
for command in crate::ports::functional_exchange_commands(self, &request)
.expect("fresh exchange identities")
{
match command {
crate::Command::CreateElement(e) => {
self.elements.insert(e.id, e);
}
crate::Command::CreateRelation(r) => {
self.relations.insert(r.id, r);
}
_ => unreachable!("functional creation only creates ports and an exchange"),
}
}
return id;
}
let mut body = RelationBody::new(kind, source, target);
if self
.elements
.get(&source)
.is_some_and(|e| e.kind() == Kind::EntryPoint)
&& let RelationBody::StateTransition {
transition_kind, ..
} = &mut body
{
*transition_kind = TransitionKind::Local;
}
self.relations.insert(
id,
Relation {
id,
alias,
name: name.into(),
description: String::new(),
body,
},
);
id
}
pub fn demo() -> Self {
crate::demo::coastal_rescue()
}
}