use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
pub use crate::ecel::{parse_condition, Condition};
pub type NodeId = String;
pub type GateId = String;
#[derive(Debug, Clone, Serialize)]
pub struct EtlDocument {
pub etdl: String,
pub info: Info,
#[serde(default)]
pub asyncapi_imports: BTreeMap<String, String>,
#[serde(default)]
pub components: Option<Components>,
pub event_trees: BTreeMap<String, EventTree>,
#[serde(default)]
pub fault_trees: Option<BTreeMap<String, FaultTree>>,
pub extensions: BTreeMap<String, serde_yaml::Value>,
}
impl<'de> Deserialize<'de> for EtlDocument {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{Error, MapAccess, Visitor};
use std::fmt;
struct DocVisitor;
impl<'de> Visitor<'de> for DocVisitor {
type Value = EtlDocument;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an ETDL document")
}
fn visit_map<A>(self, mut map: A) -> Result<EtlDocument, A::Error>
where
A: MapAccess<'de>,
{
let mut etdl: Option<String> = None;
let mut info: Option<Info> = None;
let mut asyncapi_imports: BTreeMap<String, String> = BTreeMap::new();
let mut components: Option<Components> = None;
let mut event_trees_map: BTreeMap<String, EventTree> = BTreeMap::new();
let mut event_tree_legacy: Option<EventTree> = None;
let mut fault_trees: Option<BTreeMap<String, FaultTree>> = None;
let mut extensions: BTreeMap<String, serde_yaml::Value> = BTreeMap::new();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"etdl" => {
if etdl.is_some() {
return Err(Error::duplicate_field("etdl"));
}
etdl = Some(map.next_value()?);
}
"info" => {
if info.is_some() {
return Err(Error::duplicate_field("info"));
}
info = Some(map.next_value()?);
}
"asyncapi_imports" => {
asyncapi_imports = map.next_value()?;
}
"components" => {
components = map.next_value()?;
}
"eventTrees" => {
event_trees_map = map.next_value()?;
}
"eventTree" => {
event_tree_legacy = Some(map.next_value()?);
}
"faultTrees" => {
fault_trees = map.next_value()?;
}
k if k.starts_with("x-") => {
let val: serde_yaml::Value = map.next_value()?;
extensions.insert(k.to_string(), val);
}
unknown => {
return Err(Error::custom(format!(
"unrecognized field '{}' in ETDL document; extension fields must start with 'x-'",
unknown
)));
}
}
}
let etdl = etdl.ok_or_else(|| Error::missing_field("etdl"))?;
let info = info.ok_or_else(|| Error::missing_field("info"))?;
let event_trees = match (event_trees_map.is_empty(), event_tree_legacy) {
(true, Some(tree)) => {
let mut map = BTreeMap::new();
map.insert("default".to_string(), tree);
map
}
(true, None) => {
return Err(Error::custom(
"at least one of 'eventTrees' or 'eventTree' (deprecated) must be present",
));
}
(false, None) => event_trees_map,
(false, Some(_)) => {
return Err(Error::custom(
"both 'eventTrees' and 'eventTree' (deprecated) provided; use only 'eventTrees'",
));
}
};
Ok(EtlDocument {
etdl,
info,
asyncapi_imports,
components,
event_trees,
fault_trees,
extensions,
})
}
}
deserializer.deserialize_map(DocVisitor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Info {
pub title: String,
pub version: String,
#[serde(deserialize_with = "deserialize_domain")]
pub domain: String,
#[serde(default)]
pub description: Option<String>,
}
fn deserialize_domain<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.is_empty() || !s.chars().next().unwrap().is_ascii_alphabetic() {
return Err(serde::de::Error::custom(
"domain must match ^[A-Za-z][A-Za-z0-9]*$",
));
}
if s.chars().any(|c| !c.is_ascii_alphanumeric()) {
return Err(serde::de::Error::custom(
"domain must match ^[A-Za-z][A-Za-z0-9]*$",
));
}
Ok(s)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Components {
#[serde(default)]
pub barriers: Option<BTreeMap<String, Barrier>>,
#[serde(default)]
pub operations: Option<BTreeMap<String, Operation>>,
#[serde(default)]
pub gates: Option<BTreeMap<String, Gate>>,
#[serde(default)]
pub basic_events: Option<BTreeMap<String, BasicEvent>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventTree {
#[serde(rename = "initiatingEvent")]
pub initiating_event: InitiatingEvent,
pub nodes: BTreeMap<NodeId, Node>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitiatingEvent {
pub id: String,
pub message: ExternalRef,
pub next: NodeId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Node {
#[serde(rename = "barrier")]
Barrier(Barrier),
#[serde(rename = "operation")]
Operation(Operation),
#[serde(rename = "consequence")]
Consequence(Consequence),
}
impl Node {
pub fn node_type(&self) -> &str {
match self {
Node::Barrier(_) => "barrier",
Node::Operation(_) => "operation",
Node::Consequence(_) => "consequence",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Barrier {
pub branches: Vec<Branch>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Branch {
pub outcome: String,
#[serde(deserialize_with = "deserialize_condition")]
pub condition: Condition,
#[serde(default, deserialize_with = "deserialize_optional_f64")]
pub probability: Option<f64>,
#[serde(default, alias = "probabilityOfSuccess")]
pub probability_of_success: Option<f64>,
#[serde(default, alias = "probabilityOfFailure")]
pub probability_of_failure: Option<f64>,
#[serde(default, alias = "probabilitySource")]
pub probability_source: Option<InternalRef>,
pub next: NodeId,
}
fn deserialize_condition<'de, D>(deserializer: D) -> Result<Condition, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_condition(&s).map_err(serde::de::Error::custom)
}
fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
Option::<f64>::deserialize(deserializer)
}
impl Branch {
pub fn effective_probability(&self) -> Option<f64> {
self.probability
.or(self.probability_of_success)
.or(self.probability_of_failure)
}
pub fn has_probability_source(&self) -> bool {
self.probability_source.is_some()
|| self.probability.is_some()
|| self.probability_of_success.is_some()
|| self.probability_of_failure.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Operation {
#[serde(default = "default_action")]
pub action: ActionKind,
pub handler: String,
#[serde(default)]
pub emits: Option<ExternalRef>,
pub next: NodeId,
#[serde(default, alias = "onFailure")]
pub on_failure: Option<NodeId>,
#[serde(default, alias = "onFailureProbabilitySource")]
pub on_failure_probability_source: Option<InternalRef>,
#[serde(default, alias = "retryPolicy")]
pub retry_policy: Option<RetryPolicy>,
#[serde(default, alias = "timeoutMs")]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub description: Option<String>,
}
fn default_action() -> ActionKind {
ActionKind::Execute
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ActionKind {
#[serde(rename = "execute")]
Execute,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
#[serde(default = "default_max_attempts", alias = "maxAttempts")]
pub max_attempts: u32,
#[serde(default = "default_backoff_ms", alias = "backoffMs")]
pub backoff_ms: u64,
#[serde(default, alias = "backoffStrategy")]
pub backoff_strategy: Option<BackoffStrategy>,
}
fn default_max_attempts() -> u32 {
1
}
fn default_backoff_ms() -> u64 {
0
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum BackoffStrategy {
#[serde(rename = "fixed")]
#[default]
Fixed,
#[serde(rename = "exponential")]
Exponential,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Consequence {
#[serde(rename = "operation")]
pub consequence_operation: ConsequenceOperation,
#[serde(default)]
pub channel: Option<ExternalRef>,
#[serde(default)]
pub message: Option<ExternalRef>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConsequenceOperation {
#[serde(rename = "send")]
Send,
#[serde(rename = "terminate")]
Terminate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FaultTree {
#[serde(rename = "topEvent")]
pub top_event: TopEvent,
#[serde(default)]
pub gates: Option<BTreeMap<GateId, Gate>>,
#[serde(rename = "basicEvents")]
pub basic_events: BTreeMap<String, BasicEvent>,
#[serde(default)]
pub transfers: Option<BTreeMap<String, TransferNode>>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopEvent {
pub id: String,
pub description: String,
#[serde(default)]
pub message: Option<ExternalRef>,
#[serde(rename = "rootCause")]
pub root_cause: FaultTreeNodeRef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Gate {
#[serde(rename = "type")]
pub gate_type: GateType,
pub inputs: Vec<FaultTreeNodeRef>,
#[serde(default)]
pub k: Option<u32>,
#[serde(default)]
pub description: Option<String>,
#[serde(default, alias = "inhibitCondition")]
pub inhibit_condition: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GateType {
#[serde(rename = "AND")]
And,
#[serde(rename = "OR")]
Or,
#[serde(rename = "NOT")]
Not,
#[serde(rename = "XOR")]
Xor,
#[serde(rename = "VOTING")]
Voting,
#[serde(rename = "INHIBIT")]
Inhibit,
#[serde(rename = "PRIORITY_AND")]
PriorityAnd,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BasicEventType {
#[serde(rename = "basic")]
Basic,
#[serde(rename = "house")]
House,
#[serde(rename = "undeveloped")]
Undeveloped,
#[serde(rename = "conditional")]
Conditional,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferNode {
pub target: String,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicEvent {
pub description: String,
#[serde(default)]
pub probability: Option<f64>,
#[serde(default, alias = "failureRate")]
pub failure_rate: Option<f64>,
#[serde(default, alias = "missionTime")]
pub mission_time: Option<f64>,
#[serde(default)]
pub undeveloped: Option<bool>,
#[serde(default, alias = "eventType")]
pub event_type: Option<BasicEventType>,
#[serde(default)]
pub message: Option<ExternalRef>,
}
#[derive(Debug, Clone)]
pub struct ExternalRef {
pub alias: String,
pub pointer: String,
}
#[derive(Debug, Clone)]
pub struct InternalRef {
pub pointer: String,
}
pub type FaultTreeNodeRef = String;
impl Serialize for ExternalRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}#{}", self.alias, self.pointer))
}
}
impl<'de> Deserialize<'de> for ExternalRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_external_ref(&s).map_err(serde::de::Error::custom)
}
}
fn parse_external_ref(s: &str) -> Result<ExternalRef, String> {
if let Some(hash_pos) = s.find('#') {
let alias = &s[..hash_pos];
let pointer = &s[hash_pos..];
if alias.is_empty() {
if pointer.starts_with("#/") {
return Err(format!(
"bare JSON Pointer '{}' without import alias; use InternalRef for same-document references",
pointer
));
}
return Err("empty alias in external reference".to_string());
}
if alias
.chars()
.any(|c| !c.is_ascii_alphanumeric() && c != '_')
{
return Err(format!("invalid import alias '{}'", alias));
}
Ok(ExternalRef {
alias: alias.to_string(),
pointer: pointer.to_string(),
})
} else {
Err(format!("no '#' found in external reference '{}'", s))
}
}
impl Serialize for InternalRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.pointer)
}
}
impl<'de> Deserialize<'de> for InternalRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if !s.starts_with("#/") {
return Err(serde::de::Error::custom(format!(
"invalid internal reference '{}'; must start with '#/'",
s
)));
}
Ok(InternalRef { pointer: s })
}
}
impl ExternalRef {
pub fn as_string(&self) -> String {
format!("{}#{}", self.alias, self.pointer)
}
}
impl InternalRef {
pub fn as_string(&self) -> String {
self.pointer.clone()
}
}
#[derive(Debug, Clone)]
pub enum ParsedReference {
External(ExternalRef),
Internal(InternalRef),
}
pub fn parse_reference(s: &str) -> Result<ParsedReference, String> {
if s.starts_with("#/") {
Ok(ParsedReference::Internal(InternalRef {
pointer: s.to_string(),
}))
} else if let Some(hash_pos) = s.find('#') {
let alias = &s[..hash_pos];
let pointer = &s[hash_pos..];
if alias.is_empty() || pointer.is_empty() || !pointer.starts_with('#') {
return Err(format!("invalid reference syntax: '{}'", s));
}
parse_external_ref(s).map(ParsedReference::External)
} else {
Err(format!(
"reference '{}' matches neither external nor internal format",
s
))
}
}