336 lines
11 KiB
Rust
336 lines
11 KiB
Rust
//! Existing-core expressivity experiment, not a proposed production DTO.
|
|
use oa_core::validation::{Severity, validate};
|
|
use oa_core::*;
|
|
use std::{collections::BTreeMap, fs, path::Path};
|
|
fn id(n: u128) -> Id {
|
|
Id::from_u128(0x52060906000040008000000000000000 + n)
|
|
}
|
|
fn add(p: &mut Project, n: u128, k: Kind, name: &str, parent: Option<Id>) -> Id {
|
|
let mut e = if k == Kind::Port {
|
|
Element {
|
|
id: id(n),
|
|
alias: format!("operation_probe_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
parent,
|
|
body: ElementBody::shared_in(
|
|
Layer::Sa,
|
|
SharedBody::Port {
|
|
contract: PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
},
|
|
legacy: None,
|
|
},
|
|
),
|
|
}
|
|
} else {
|
|
p.make_element(k, Layer::Sa, name, parent).unwrap()
|
|
};
|
|
e.id = id(n);
|
|
e.alias = format!("operation_probe_{n}");
|
|
p.elements.insert(e.id, e);
|
|
id(n)
|
|
}
|
|
fn body(p: &mut Project, n: u128, b: SharedBody) {
|
|
*p.elements.get_mut(&id(n)).unwrap().shared_mut().unwrap() = b;
|
|
}
|
|
fn rel(p: &mut Project, n: u128, k: RelationKind, a: u128, b: u128) {
|
|
p.relations.insert(
|
|
id(n),
|
|
Relation {
|
|
id: id(n),
|
|
alias: format!("operation_probe_{n}"),
|
|
name: format!("{k:?} {n}"),
|
|
description: String::new(),
|
|
body: RelationBody::new(k, id(a), id(b)),
|
|
},
|
|
);
|
|
}
|
|
fn valid(p: &Project) {
|
|
let errors: Vec<_> = validate(p)
|
|
.into_iter()
|
|
.filter(|i| i.severity == Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
}
|
|
fn message(p: &Project, n: u128) -> &OccurrenceBody {
|
|
let Some(SharedBody::Occurrence { body, .. }) = p.elements[&id(n)].shared() else {
|
|
panic!()
|
|
};
|
|
body
|
|
}
|
|
fn set_exchange(p: &mut Project, n: u128, target: Id) {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { exchange, .. },
|
|
..
|
|
}) = p.elements.get_mut(&id(n)).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*exchange = Some(target);
|
|
}
|
|
fn initial() -> Project {
|
|
let mut p = Project::empty();
|
|
p.id = id(1);
|
|
p.name = "Two operations, one interface".into();
|
|
add(&mut p, 10, Kind::SystemActor, "Operator console", None);
|
|
add(&mut p, 11, Kind::System, "Valve service", None);
|
|
for (port, owner) in [(12, 10), (13, 11)] {
|
|
add(&mut p, port, Kind::Port, "Service socket", Some(id(owner)));
|
|
body(
|
|
&mut p,
|
|
port,
|
|
SharedBody::Port {
|
|
contract: PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
},
|
|
legacy: None,
|
|
},
|
|
);
|
|
}
|
|
for (operation, name) in [(20, "Reset"), (21, "ReadStatus")] {
|
|
add(&mut p, operation, Kind::ExchangeItem, name, None);
|
|
body(
|
|
&mut p,
|
|
operation,
|
|
SharedBody::ExchangeItem {
|
|
kind: ExchangeItemKind::Information,
|
|
mechanism: ExchangeMechanism::Operation,
|
|
supertype: None,
|
|
},
|
|
);
|
|
}
|
|
add(&mut p, 22, Kind::Interface, "Valve commands", None);
|
|
for (allocation, item) in [(30, 20), (31, 21)] {
|
|
rel(
|
|
&mut p,
|
|
allocation,
|
|
RelationKind::InterfaceItemAllocation,
|
|
22,
|
|
item,
|
|
);
|
|
let RelationBody::InterfaceItemAllocation { transmission, .. } =
|
|
&mut p.relations.get_mut(&id(allocation)).unwrap().body
|
|
else {
|
|
panic!()
|
|
};
|
|
*transmission = TransmissionProtocol::Synchronous;
|
|
}
|
|
rel(&mut p, 32, RelationKind::InterfaceRequirement, 12, 22);
|
|
rel(&mut p, 33, RelationKind::InterfaceProvision, 13, 22);
|
|
rel(&mut p, 34, RelationKind::BoundaryExchange, 12, 13);
|
|
p.relations.get_mut(&id(34)).unwrap().body = RelationBody::BoundaryExchange {
|
|
source: id(12),
|
|
target: id(13),
|
|
oriented: false,
|
|
items: vec![id(20), id(21)],
|
|
};
|
|
add(&mut p, 40, Kind::Scenario, "Reset then read status", None);
|
|
body(
|
|
&mut p,
|
|
40,
|
|
SharedBody::Scenario {
|
|
viewpoint: ScenarioViewpoint::BoundaryInterface,
|
|
context: None,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
steps: vec![id(43), id(44)],
|
|
},
|
|
);
|
|
for (line, participant) in [(41, 10), (42, 11)] {
|
|
add(
|
|
&mut p,
|
|
line,
|
|
Kind::Lifeline,
|
|
"Service participant",
|
|
Some(id(40)),
|
|
);
|
|
body(
|
|
&mut p,
|
|
line,
|
|
SharedBody::Lifeline {
|
|
represented: Some(id(participant)),
|
|
},
|
|
);
|
|
}
|
|
for (call, name) in [(43, "Call Reset"), (44, "Call ReadStatus")] {
|
|
add(&mut p, call, Kind::Occurrence, name, Some(id(40)));
|
|
body(
|
|
&mut p,
|
|
call,
|
|
SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: Some(id(34)),
|
|
sender: Some(id(41)),
|
|
receiver: Some(id(42)),
|
|
message_kind: MessageKind::Call,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
},
|
|
);
|
|
}
|
|
valid(&p);
|
|
p
|
|
}
|
|
fn refined(p: &Project) -> Project {
|
|
let mut q = p.clone();
|
|
for (work, owner, name) in [
|
|
(50, 10, "Issue service request"),
|
|
(51, 11, "Execute service request"),
|
|
] {
|
|
add(&mut q, work, Kind::Function, name, None);
|
|
rel(&mut q, work + 10, RelationKind::Allocation, owner, work);
|
|
}
|
|
for (item, output, input, exchange) in [(20, 70, 71, 80), (21, 72, 73, 81)] {
|
|
add(&mut q, output, Kind::Port, "Operation output", Some(id(50)));
|
|
add(&mut q, input, Kind::Port, "Operation input", Some(id(51)));
|
|
body(
|
|
&mut q,
|
|
output,
|
|
SharedBody::Port {
|
|
contract: PortContract::FunctionOutput {
|
|
outgoing_items: vec![id(item)],
|
|
},
|
|
legacy: None,
|
|
},
|
|
);
|
|
body(
|
|
&mut q,
|
|
input,
|
|
SharedBody::Port {
|
|
contract: PortContract::FunctionInput {
|
|
incoming_items: vec![id(item)],
|
|
},
|
|
legacy: None,
|
|
},
|
|
);
|
|
rel(&mut q, exchange, RelationKind::FunctionalExchange, 50, 51);
|
|
q.relations.get_mut(&id(exchange)).unwrap().body = RelationBody::FunctionalExchange {
|
|
source: id(50),
|
|
target: id(51),
|
|
source_port: id(output),
|
|
target_port: id(input),
|
|
items: vec![id(item)],
|
|
};
|
|
rel(
|
|
&mut q,
|
|
output + 20,
|
|
RelationKind::PortAllocation,
|
|
12,
|
|
output,
|
|
);
|
|
rel(&mut q, input + 20, RelationKind::PortAllocation, 13, input);
|
|
rel(
|
|
&mut q,
|
|
exchange + 20,
|
|
RelationKind::ExchangeAllocation,
|
|
34,
|
|
exchange,
|
|
);
|
|
}
|
|
set_exchange(&mut q, 43, id(80));
|
|
set_exchange(&mut q, 44, id(81));
|
|
valid(&q);
|
|
q
|
|
}
|
|
fn write(p: &Project, out: &Path, name: &str) {
|
|
let source = language::format(p);
|
|
let bundle = source_project::SourceBundle {
|
|
entry: "entry.arcadia".into(),
|
|
files: BTreeMap::from([("entry.arcadia".into(), source.clone())]),
|
|
};
|
|
assert_eq!(
|
|
&source_project::compile_bundle(&bundle, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
p
|
|
);
|
|
assert_eq!(
|
|
&serde_json::from_value::<Project>(serde_json::to_value(p).unwrap()).unwrap(),
|
|
p
|
|
);
|
|
fs::write(out.join(format!("{name}.arcadia")), source).unwrap();
|
|
fs::write(
|
|
out.join(format!("{name}.json")),
|
|
serde_json::to_vec_pretty(p).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
fn main() {
|
|
let directory = std::env::args().nth(1).expect("probe OUTPUT_DIRECTORY");
|
|
let out = Path::new(&directory);
|
|
fs::create_dir_all(out).unwrap();
|
|
let p = initial();
|
|
write(&p, out, "boundary-only");
|
|
assert_eq!(
|
|
message(&p, 43),
|
|
message(&p, 44),
|
|
"Only prose distinguishes the intended operations"
|
|
);
|
|
assert!(!references::element_references(&p.elements[&id(43)]).contains(&id(20)));
|
|
assert!(!references::element_references(&p.elements[&id(44)]).contains(&id(21)));
|
|
for target in [20, 30] {
|
|
let mut invalid = p.clone();
|
|
set_exchange(&mut invalid, 43, id(target));
|
|
assert!(
|
|
validate(&invalid)
|
|
.iter()
|
|
.any(|i| i.code == "occurrence_exchange" && i.element == Some(id(43)))
|
|
);
|
|
}
|
|
let mut proposed = serde_json::to_value(message(&p, 43)).unwrap();
|
|
proposed["Exchange"]["operation"] = serde_json::to_value(id(30)).unwrap();
|
|
assert!(
|
|
serde_json::from_value::<OccurrenceBody>(proposed)
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("unknown field")
|
|
);
|
|
let mut session = Session::new(p.clone());
|
|
let mut reset = p.elements[&id(20)].clone();
|
|
reset.name = "EmergencyReset".into();
|
|
session
|
|
.execute(Command::EditElement(id(20), reset))
|
|
.unwrap();
|
|
assert_eq!(message(session.project(), 43), message(&p, 43));
|
|
session.execute(Command::Delete(id(20))).unwrap();
|
|
assert_eq!(message(session.project(), 43), message(&p, 43));
|
|
assert!(session.project().elements.contains_key(&id(43)));
|
|
valid(session.project());
|
|
write(session.project(), out, "after-reset-deletion");
|
|
let q = refined(&p);
|
|
write(&q, out, "explicit-functional-refinement");
|
|
assert_eq!(q.relations[&id(34)], p.relations[&id(34)]);
|
|
assert_ne!(message(&q, 43), message(&q, 44));
|
|
for n in [43, 44] {
|
|
assert!(
|
|
references::element_references(&q.elements[&id(n)])
|
|
.iter()
|
|
.any(|v| [id(80), id(81)].contains(v))
|
|
);
|
|
}
|
|
println!(
|
|
"PASS validated boundary-only model: {} elements / {} relations, two parameterless Operations",
|
|
p.elements.len(),
|
|
p.relations.len()
|
|
);
|
|
println!(
|
|
"PASS identical typed Calls; item/allocation exchange substitutions rejected; operation DTO field rejected"
|
|
);
|
|
println!(
|
|
"PASS operation rename/delete leaves intended Call's typed body unchanged; Call remains structurally valid"
|
|
);
|
|
println!(
|
|
"PASS distinct FE Calls require this explicit refinement: +{} elements / +{} relations; same original boundary",
|
|
q.elements.len() - p.elements.len(),
|
|
q.relations.len() - p.relations.len()
|
|
);
|
|
println!("PASS all three complete source/JSON round trips; no production mutation");
|
|
}
|