#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntityType {
Ui,
Processor,
ReadModel,
Command,
Event,
}
impl EntityType {
fn from_str(s: &str) -> Option<Self> {
match s {
"ui" => Some(EntityType::Ui),
"pcr" | "processor" => Some(EntityType::Processor),
"rmo" | "readmodel" => Some(EntityType::ReadModel),
"cmd" | "command" => Some(EntityType::Command),
"evt" | "event" => Some(EntityType::Event),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct EmFrame {
pub name: String,
pub entity_type: EntityType,
pub entity_id: String,
pub source_refs: Vec<String>,
pub is_reset: bool,
#[allow(dead_code)]
pub inline_data: Option<String>,
#[allow(dead_code)]
pub data_ref: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EmDataEntity {
#[allow(dead_code)]
pub name: String,
#[allow(dead_code)]
pub data: String,
}
#[derive(Debug)]
pub struct EventModelDiagram {
pub title: Option<String>,
pub frames: Vec<EmFrame>,
#[allow(dead_code)]
pub data_entities: Vec<EmDataEntity>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<EventModelDiagram> {
let mut title: Option<String> = None;
let mut frames: Vec<EmFrame> = Vec::new();
let mut data_entities: Vec<EmDataEntity> = Vec::new();
let mut data_block_name: Option<String> = None;
let mut data_block_lines: Vec<String> = Vec::new();
let mut in_data_block = false;
let mut in_header = true;
for raw in input.lines() {
let line = if let Some(p) = raw.find("%%") {
&raw[..p]
} else {
raw
};
let trimmed = line.trim();
if in_header {
if trimmed.starts_with("eventmodeling") {
in_header = false;
}
continue;
}
if in_data_block {
if trimmed == "}" {
if let Some(name) = data_block_name.take() {
data_entities.push(EmDataEntity {
name,
data: data_block_lines.join("\n"),
});
}
data_block_lines.clear();
in_data_block = false;
} else {
data_block_lines.push(trimmed.to_string());
}
continue;
}
if trimmed.is_empty() {
continue;
}
if let Some(rest) = trimmed
.strip_prefix("title ")
.or_else(|| trimmed.strip_prefix("title\t"))
{
title = Some(rest.trim().to_string());
continue;
}
if trimmed == "title" {
title = Some(String::new());
continue;
}
if trimmed.starts_with("accTitle") || trimmed.starts_with("accDescr") {
continue;
}
if let Some(rest) = trimmed
.strip_prefix("data ")
.or_else(|| trimmed.strip_prefix("data\t"))
{
let name_tok = rest.split_whitespace().next().unwrap_or("").to_string();
if let Some(brace_pos) = rest.find('{') {
let after = rest[brace_pos + 1..].trim();
let content = if let Some(end) = after.rfind('}') {
after[..end].trim().to_string()
} else {
after.to_string()
};
if !name_tok.is_empty() {
data_entities.push(EmDataEntity {
name: name_tok,
data: content,
});
}
} else if !name_tok.is_empty() {
data_block_name = Some(name_tok);
data_block_lines.clear();
in_data_block = true;
}
continue;
}
let (is_reset, rest_after_kw) = if let Some(r) = trimmed
.strip_prefix("rf ")
.or_else(|| trimmed.strip_prefix("rf\t"))
.or_else(|| trimmed.strip_prefix("resetframe "))
.or_else(|| trimmed.strip_prefix("resetframe\t"))
{
(true, r.trim())
} else if let Some(r) = trimmed
.strip_prefix("tf ")
.or_else(|| trimmed.strip_prefix("tf\t"))
.or_else(|| trimmed.strip_prefix("timeframe "))
.or_else(|| trimmed.strip_prefix("timeframe\t"))
{
(false, r.trim())
} else {
continue;
};
if let Some(frame) = parse_frame_line(rest_after_kw, is_reset) {
frames.push(frame);
}
}
crate::error::ParseResult::ok(EventModelDiagram {
title,
frames,
data_entities,
})
}
fn parse_frame_line(s: &str, is_reset: bool) -> Option<EmFrame> {
let tokens = tokenize_frame(s);
let mut pos = 0;
let name = tokens.get(pos)?.clone();
if !name.chars().all(|c| c.is_ascii_digit()) || name.is_empty() || name.len() > 3 {
return None;
}
pos += 1;
let type_tok = tokens.get(pos)?;
let entity_type = EntityType::from_str(type_tok)?;
pos += 1;
let entity_id = tokens.get(pos)?.clone();
if entity_id.is_empty() {
return None;
}
pos += 1;
let mut source_refs: Vec<String> = Vec::new();
while pos < tokens.len() {
let tok = &tokens[pos];
if tok == "->>" || tok == "->" {
pos += 1;
if let Some(ref_id) = tokens.get(pos) {
if ref_id.chars().all(|c| c.is_ascii_digit()) && !ref_id.is_empty() {
source_refs.push(ref_id.clone());
pos += 1;
}
}
} else {
break;
}
}
let mut data_ref: Option<String> = None;
if pos < tokens.len() && tokens[pos] == "[[" {
pos += 1;
if let Some(dr) = tokens.get(pos) {
data_ref = Some(dr.clone());
pos += 1;
}
if tokens.get(pos).map(|t| t == "]]").unwrap_or(false) {
pos += 1;
}
}
let inline_data = if pos < tokens.len() {
let rest: String = tokens[pos..].join(" ");
extract_inline_data(&rest)
} else {
None
};
let _ = tokens.len();
Some(EmFrame {
name,
entity_type,
entity_id,
source_refs,
is_reset,
inline_data,
data_ref,
})
}
fn tokenize_frame(s: &str) -> Vec<String> {
let mut tokens: Vec<String> = Vec::new();
let mut chars = s.chars().peekable();
loop {
while chars.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
chars.next();
}
let Some(&ch) = chars.peek() else { break };
if ch == '-' {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
if chars.peek() == Some(&'>') {
chars.next();
tokens.push("->>".to_string());
} else {
tokens.push("->".to_string());
}
} else {
tokens.push("-".to_string());
}
} else if ch == '[' {
chars.next();
if chars.peek() == Some(&'[') {
chars.next();
tokens.push("[[".to_string());
} else {
tokens.push("[".to_string());
}
} else if ch == ']' {
chars.next();
if chars.peek() == Some(&']') {
chars.next();
tokens.push("]]".to_string());
} else {
tokens.push("]".to_string());
}
} else if ch == '{' || ch == '`' {
let rest: String = chars.collect();
tokens.push(rest);
break;
} else {
let mut tok = String::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() || c == '[' || c == ']' || c == '-' {
break;
}
tok.push(c);
chars.next();
}
if !tok.is_empty() {
tokens.push(tok);
}
}
}
tokens
}
fn extract_inline_data(s: &str) -> Option<String> {
let s = s.trim();
if let Some(rest_brace) = s.strip_prefix('{') {
if let Some(end) = s.rfind('}') {
return Some(s[1..end].trim().to_string());
}
return Some(rest_brace.trim().to_string());
}
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
return Some(s[1..s.len() - 1].to_string());
}
if let Some(rest_bt) = s.strip_prefix('`') {
if let Some(end_bt) = rest_bt.find('`') {
let after = s[end_bt + 2..].trim();
return extract_inline_data(after);
}
}
None
}