262 lines
9.6 KiB
Rust
262 lines
9.6 KiB
Rust
//! Typed authored workflow inputs; no stored command is executable on recovery.
|
|
use super::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
enum Saved {
|
|
Semantic(serde_json::Value),
|
|
Ports(serde_json::Value),
|
|
Relation {
|
|
base: String,
|
|
id: Id,
|
|
name: String,
|
|
kind: RelationKind,
|
|
source: Option<Id>,
|
|
target: Option<Id>,
|
|
condition: String,
|
|
layer: Layer,
|
|
items: Vec<Id>,
|
|
source_port: PortChoice,
|
|
target_port: PortChoice,
|
|
oriented: bool,
|
|
},
|
|
ManualTrace {
|
|
base: String,
|
|
name: String,
|
|
source: Id,
|
|
target: Option<Id>,
|
|
},
|
|
Transition {
|
|
base: String,
|
|
request: TransitionRequest,
|
|
#[serde(default)]
|
|
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
|
|
},
|
|
}
|
|
pub(crate) fn endpoint(p: &Project, port: PortChoice) -> EndpointChoice {
|
|
let label = match port {
|
|
PortChoice::Auto => "Automatic · reuse only if unique".into(),
|
|
PortChoice::Create => "Create a new port".into(),
|
|
PortChoice::Reuse(id) => format!("Reuse {}", choice(p, id).label),
|
|
};
|
|
EndpointChoice {
|
|
choice: port,
|
|
label,
|
|
}
|
|
}
|
|
fn check(p: &Project, base: &str) -> Result<(), String> {
|
|
if project_fingerprint(p) != base {
|
|
return Err(
|
|
"Workflow recovery does not match the accepted model identity and revision.".into(),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
impl Workflow {
|
|
pub(crate) fn from_recovery_after_source5_upgrade(
|
|
p: &Project,
|
|
legacy_base: &str,
|
|
value: serde_json::Value,
|
|
) -> Result<Self, String> {
|
|
let mut saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
|
match &mut saved {
|
|
Saved::Semantic(value) => {
|
|
return crate::semantic_review::State::from_recovery_after_source5_upgrade(
|
|
p,
|
|
legacy_base,
|
|
value.take(),
|
|
)
|
|
.map(|state| Self::Semantic(Box::new(state)));
|
|
}
|
|
Saved::Ports(value) => {
|
|
return crate::port_workflow::State::from_recovery_after_source5_upgrade(
|
|
p,
|
|
legacy_base,
|
|
value.take(),
|
|
)
|
|
.map(|state| Self::Ports(Box::new(state)));
|
|
}
|
|
Saved::Relation { base, .. }
|
|
| Saved::ManualTrace { base, .. }
|
|
| Saved::Transition { base, .. } => {
|
|
if base != legacy_base {
|
|
return Err("Workflow recovery does not match the accepted source-5 model identity and revision.".into());
|
|
}
|
|
*base = project_fingerprint(p);
|
|
}
|
|
}
|
|
Self::from_recovery(p, serde_json::to_value(saved).map_err(|e| e.to_string())?)
|
|
}
|
|
pub fn recovery_snapshot(&self) -> Result<serde_json::Value, String> {
|
|
let saved = match self {
|
|
Self::Semantic(state) => Saved::Semantic(state.recovery_snapshot()?),
|
|
Self::Ports(state) => Saved::Ports(state.recovery_snapshot()?),
|
|
Self::Relation(d) => Saved::Relation {
|
|
base: d.base_fingerprint.clone(),
|
|
id: d.id,
|
|
name: d.name.clone(),
|
|
kind: d.kind,
|
|
source: d.source.as_ref().map(|c| c.id),
|
|
target: d.target.as_ref().map(|c| c.id),
|
|
condition: d.condition.text(),
|
|
layer: d.layer,
|
|
items: d.items.clone(),
|
|
source_port: d.source_port.choice,
|
|
target_port: d.target_port.choice,
|
|
oriented: d.oriented,
|
|
},
|
|
Self::ManualTrace {
|
|
base_fingerprint,
|
|
name,
|
|
source,
|
|
target,
|
|
} => Saved::ManualTrace {
|
|
base: base_fingerprint.clone(),
|
|
name: name.clone(),
|
|
source: source.id,
|
|
target: target.as_ref().map(|c| c.id),
|
|
},
|
|
Self::Transition(d) => Saved::Transition {
|
|
base: d.plan.base_fingerprint.clone(),
|
|
request: d.request.clone(),
|
|
communication_ports: d.communication_ports.clone(),
|
|
},
|
|
};
|
|
serde_json::to_value(saved).map_err(|e| e.to_string())
|
|
}
|
|
pub fn from_recovery(p: &Project, value: serde_json::Value) -> Result<Self, String> {
|
|
match serde_json::from_value::<Saved>(value).map_err(|e| e.to_string())? {
|
|
Saved::Semantic(value) => crate::semantic_review::State::from_recovery(p, value)
|
|
.map(|state| Self::Semantic(Box::new(state))),
|
|
Saved::Ports(value) => crate::port_workflow::State::from_recovery(p, value)
|
|
.map(|state| Self::Ports(Box::new(state))),
|
|
Saved::Relation {
|
|
base,
|
|
id,
|
|
name,
|
|
kind,
|
|
source,
|
|
target,
|
|
condition,
|
|
layer,
|
|
items,
|
|
source_port,
|
|
target_port,
|
|
oriented,
|
|
} => {
|
|
check(p, &base)?;
|
|
if items.len() > crate::source_workspace::LIMITS.model.max_records {
|
|
return Err("Recovered relationship exceeds item limits.".into());
|
|
}
|
|
let mut d = Relationship {
|
|
base_fingerprint: base,
|
|
id,
|
|
name,
|
|
kind,
|
|
source: source.map(|id| choice(p, id)),
|
|
target: target.map(|id| choice(p, id)),
|
|
condition: text_editor::Content::with_text(&condition),
|
|
layer,
|
|
items,
|
|
source_port: endpoint(p, source_port),
|
|
target_port: endpoint(p, target_port),
|
|
oriented,
|
|
sources: vec![],
|
|
targets: vec![],
|
|
};
|
|
d.refresh_choices(p);
|
|
Ok(Self::Relation(Box::new(d)))
|
|
}
|
|
Saved::ManualTrace {
|
|
base,
|
|
name,
|
|
source,
|
|
target,
|
|
} => {
|
|
check(p, &base)?;
|
|
let mut workflow = Self::manual_trace(p, source)?;
|
|
if let Self::ManualTrace {
|
|
name: n, target: t, ..
|
|
} = &mut workflow
|
|
{
|
|
*n = name;
|
|
*t = target.map(|id| choice(p, id));
|
|
}
|
|
Ok(workflow)
|
|
}
|
|
Saved::Transition {
|
|
base,
|
|
request,
|
|
mut communication_ports,
|
|
} => {
|
|
check(p, &base)?;
|
|
if communication_ports.len() > crate::source_workspace::LIMITS.model.max_records {
|
|
return Err("Recovered transition exceeds endpoint draft limits.".into());
|
|
}
|
|
for (id, draft) in communication::from_request(&request) {
|
|
communication_ports.entry(id).or_insert(draft);
|
|
}
|
|
let plan = plan_transition(p, &request).map_err(|e| e.to_string())?;
|
|
let mut transition = Transition {
|
|
request,
|
|
plan,
|
|
communication_ports,
|
|
};
|
|
transition.sync_communication_ports();
|
|
transition.plan =
|
|
plan_transition(p, &transition.request).map_err(|e| e.to_string())?;
|
|
Ok(Self::Transition(Box::new(transition)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model() {
|
|
let mut p = Project::empty();
|
|
let a = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Source", None)
|
|
.unwrap();
|
|
let mut relation = Workflow::relationship(&p, a, Layer::Oa);
|
|
if let Workflow::Relation(d) = &mut relation {
|
|
d.name = " unfinished name ".into();
|
|
d.condition = text_editor::Content::with_text("raw (\n unfinished");
|
|
d.target = None;
|
|
}
|
|
let saved = relation.recovery_snapshot().unwrap();
|
|
let restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
|
let trace = Workflow::manual_trace(&p, a).unwrap();
|
|
let saved_trace = trace.recovery_snapshot().unwrap();
|
|
assert_eq!(
|
|
Workflow::from_recovery(&p, saved_trace.clone())
|
|
.unwrap()
|
|
.recovery_snapshot()
|
|
.unwrap(),
|
|
saved_trace
|
|
);
|
|
p.name.push('!');
|
|
assert!(Workflow::from_recovery(&p, saved).is_err());
|
|
assert!(Workflow::from_recovery(&p, saved_trace).is_err());
|
|
}
|
|
#[test]
|
|
fn transition_recovery_replans_typed_request_without_persisted_commands() {
|
|
let mut p = Project::empty();
|
|
let a = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Source", None)
|
|
.unwrap();
|
|
let workflow = Workflow::transition(&p, a).unwrap();
|
|
let saved = workflow.recovery_snapshot().unwrap();
|
|
assert!(!serde_json::to_string(&saved).unwrap().contains("commands"));
|
|
let Workflow::Transition(restored) = Workflow::from_recovery(&p, saved).unwrap() else {
|
|
panic!()
|
|
};
|
|
assert_eq!(restored.plan.base_fingerprint, project_fingerprint(&p));
|
|
assert_eq!(restored.request.sources, vec![a]);
|
|
}
|
|
}
|