use crate::Error;
use crate::parser::common::strip_inline_comment;
use crate::requirement_diagram::{
Element, RelationshipKind, Requirement, RequirementDiagram, RequirementKind,
RequirementRelationship, Risk, VerifyMethod,
};
pub fn parse(src: &str) -> Result<RequirementDiagram, Error> {
let mut header_seen = false;
let mut diag = RequirementDiagram::default();
let mut in_block = false;
let mut block_kind: BlockKind = BlockKind::Requirement(RequirementKind::default());
let mut block_name = String::new();
let mut block_lines: Vec<String> = Vec::new();
for raw in src.lines() {
let stripped = strip_inline_comment(raw);
let trimmed = stripped.trim();
if !header_seen {
if trimmed.is_empty() || trimmed.starts_with("%%") {
continue;
}
if !trimmed.eq_ignore_ascii_case("requirementDiagram") {
return Err(Error::ParseError(format!(
"expected `requirementDiagram` header, got {trimmed:?}"
)));
}
header_seen = true;
continue;
}
if trimmed.is_empty() || trimmed.starts_with("%%") {
continue;
}
if trimmed.starts_with("accTitle") || trimmed.starts_with("accDescr") {
continue;
}
if in_block {
if trimmed == "}" {
in_block = false;
match block_kind {
BlockKind::Requirement(kind) => {
let req =
parse_requirement_block(kind, &block_name, &block_lines)?;
diag.requirements.push(req);
}
BlockKind::Element => {
let elem = parse_element_block(&block_name, &block_lines)?;
diag.elements.push(elem);
}
}
block_lines.clear();
block_name.clear();
} else {
block_lines.push(trimmed.to_string());
}
continue;
}
if let Some((kind, name)) = try_parse_block_header(trimmed) {
in_block = true;
block_kind = kind;
block_name = name;
block_lines.clear();
continue;
}
if let Some(rel) = try_parse_relationship(trimmed)? {
diag.relationships.push(rel);
continue;
}
}
if !header_seen {
return Err(Error::ParseError(
"missing `requirementDiagram` header line".to_string(),
));
}
Ok(diag)
}
#[derive(Debug, Clone, Copy)]
enum BlockKind {
Requirement(RequirementKind),
Element,
}
fn try_parse_block_header(line: &str) -> Option<(BlockKind, String)> {
let body = if let Some(b) = line.strip_suffix('{') {
b.trim()
} else {
return None;
};
let (keyword, rest) = body.split_once(char::is_whitespace)?;
let name = rest.trim().to_string();
if name.is_empty() {
return None;
}
let kind = match keyword.to_lowercase().as_str() {
"requirement" => BlockKind::Requirement(RequirementKind::Requirement),
"functionalrequirement" => BlockKind::Requirement(RequirementKind::Functional),
"interfacerequirement" => BlockKind::Requirement(RequirementKind::Interface),
"performancerequirement" => BlockKind::Requirement(RequirementKind::Performance),
"physicalrequirement" => BlockKind::Requirement(RequirementKind::Physical),
"designconstraint" => BlockKind::Requirement(RequirementKind::DesignConstraint),
"element" => BlockKind::Element,
_ => return None,
};
Some((kind, name))
}
fn parse_requirement_block(
kind: RequirementKind,
name: &str,
lines: &[String],
) -> Result<Requirement, Error> {
let mut id: Option<String> = None;
let mut text: Option<String> = None;
let mut risk: Option<Risk> = None;
let mut verify_method: Option<VerifyMethod> = None;
for line in lines {
let trimmed = line.trim();
if let Some(val) = trimmed.strip_prefix("id:") {
id = Some(val.trim().to_string());
} else if let Some(val) = trimmed.strip_prefix("text:") {
text = Some(val.trim().to_string());
} else if let Some(val) = trimmed.strip_prefix("risk:") {
risk = parse_risk(val.trim());
} else if let Some(val) = trimmed.strip_prefix("verifymethod:") {
verify_method = parse_verify_method(val.trim());
}
}
let id = id.ok_or_else(|| {
Error::ParseError(format!(
"requirement {name:?} is missing required `id:` field"
))
})?;
let text = text.ok_or_else(|| {
Error::ParseError(format!(
"requirement {name:?} is missing required `text:` field"
))
})?;
Ok(Requirement {
kind,
name: name.to_string(),
id,
text,
risk,
verify_method,
})
}
fn parse_element_block(name: &str, lines: &[String]) -> Result<Element, Error> {
let mut kind: Option<String> = None;
let mut docref: Option<String> = None;
for line in lines {
let trimmed = line.trim();
if let Some(val) = trimmed.strip_prefix("type:") {
kind = Some(val.trim().to_string());
} else if let Some(val) = trimmed.strip_prefix("docref:") {
docref = Some(val.trim().to_string());
}
}
let kind = kind.ok_or_else(|| {
Error::ParseError(format!(
"element {name:?} is missing required `type:` field"
))
})?;
Ok(Element {
name: name.to_string(),
kind,
docref,
})
}
fn try_parse_relationship(line: &str) -> Result<Option<RequirementRelationship>, Error> {
let Some(dash_pos) = line.find(" - ") else {
return Ok(None);
};
let source = line[..dash_pos].trim().to_string();
let after_dash = &line[dash_pos + 3..];
let Some(arrow_pos) = after_dash.find(" -> ") else {
return Err(Error::ParseError(format!(
"malformed relationship — expected `source - kind -> target`, got {line:?}"
)));
};
let kind_str = after_dash[..arrow_pos].trim();
let target = after_dash[arrow_pos + 4..].trim().to_string();
if source.is_empty() || target.is_empty() {
return Err(Error::ParseError(format!(
"malformed relationship — source or target is empty in {line:?}"
)));
}
let kind = match kind_str.to_lowercase().as_str() {
"contains" => RelationshipKind::Contains,
"copies" => RelationshipKind::Copies,
"derives" => RelationshipKind::Derives,
"satisfies" => RelationshipKind::Satisfies,
"verifies" => RelationshipKind::Verifies,
"refines" => RelationshipKind::Refines,
"traces" => RelationshipKind::Traces,
other => {
return Err(Error::ParseError(format!(
"unknown relationship kind {other:?} in {line:?}"
)));
}
};
Ok(Some(RequirementRelationship {
source,
target,
kind,
}))
}
fn parse_risk(s: &str) -> Option<Risk> {
match s.to_lowercase().as_str() {
"low" => Some(Risk::Low),
"medium" => Some(Risk::Medium),
"high" => Some(Risk::High),
_ => None,
}
}
fn parse_verify_method(s: &str) -> Option<VerifyMethod> {
match s.to_lowercase().as_str() {
"analysis" => Some(VerifyMethod::Analysis),
"inspection" => Some(VerifyMethod::Inspection),
"test" => Some(VerifyMethod::Test),
"demonstration" => Some(VerifyMethod::Demonstration),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
const HEADER: &str = "requirementDiagram\n";
fn minimal_req(name: &str) -> String {
format!(
"{HEADER} requirement {name} {{\n id: 1\n text: some text.\n }}"
)
}
#[test]
fn parses_minimal_requirement_diagram() {
let src = minimal_req("r1");
let diag = parse(&src).unwrap();
assert_eq!(diag.requirements.len(), 1);
assert_eq!(diag.requirements[0].name, "r1");
assert_eq!(diag.requirements[0].id, "1");
assert_eq!(diag.requirements[0].text, "some text.");
assert_eq!(diag.requirements[0].kind, RequirementKind::Requirement);
}
#[test]
fn parses_all_six_requirement_kinds() {
let src = format!(
"{HEADER}\
requirement r1 {{\n id: 1\n text: t.\n}}\n\
functionalRequirement r2 {{\n id: 2\n text: t.\n}}\n\
interfaceRequirement r3 {{\n id: 3\n text: t.\n}}\n\
performanceRequirement r4 {{\n id: 4\n text: t.\n}}\n\
physicalRequirement r5 {{\n id: 5\n text: t.\n}}\n\
designConstraint r6 {{\n id: 6\n text: t.\n}}"
);
let diag = parse(&src).unwrap();
assert_eq!(diag.requirements.len(), 6);
assert_eq!(diag.requirements[0].kind, RequirementKind::Requirement);
assert_eq!(diag.requirements[1].kind, RequirementKind::Functional);
assert_eq!(diag.requirements[2].kind, RequirementKind::Interface);
assert_eq!(diag.requirements[3].kind, RequirementKind::Performance);
assert_eq!(diag.requirements[4].kind, RequirementKind::Physical);
assert_eq!(diag.requirements[5].kind, RequirementKind::DesignConstraint);
}
#[test]
fn parses_element_with_type_only() {
let src = format!("{HEADER}element e1 {{\n type: simulation\n}}");
let diag = parse(&src).unwrap();
assert_eq!(diag.elements.len(), 1);
assert_eq!(diag.elements[0].name, "e1");
assert_eq!(diag.elements[0].kind, "simulation");
assert!(diag.elements[0].docref.is_none());
}
#[test]
fn parses_element_with_type_and_docref() {
let src = format!(
"{HEADER}element e2 {{\n type: word doc\n docref: reqs/test_entity\n}}"
);
let diag = parse(&src).unwrap();
assert_eq!(diag.elements.len(), 1);
assert_eq!(diag.elements[0].kind, "word doc");
assert_eq!(
diag.elements[0].docref,
Some("reqs/test_entity".to_string())
);
}
#[test]
fn parses_all_seven_relationship_kinds() {
let src = format!(
"{HEADER}\
a - contains -> b\n\
a - copies -> b\n\
a - derives -> b\n\
a - satisfies -> b\n\
a - verifies -> b\n\
a - refines -> b\n\
a - traces -> b"
);
let diag = parse(&src).unwrap();
assert_eq!(diag.relationships.len(), 7);
assert_eq!(diag.relationships[0].kind, RelationshipKind::Contains);
assert_eq!(diag.relationships[1].kind, RelationshipKind::Copies);
assert_eq!(diag.relationships[2].kind, RelationshipKind::Derives);
assert_eq!(diag.relationships[3].kind, RelationshipKind::Satisfies);
assert_eq!(diag.relationships[4].kind, RelationshipKind::Verifies);
assert_eq!(diag.relationships[5].kind, RelationshipKind::Refines);
assert_eq!(diag.relationships[6].kind, RelationshipKind::Traces);
}
#[test]
fn parses_risk_and_verifymethod_fields() {
let src = format!(
"{HEADER}requirement r1 {{\n\
id: 1\n\
text: t.\n\
risk: medium\n\
verifymethod: inspection\n\
}}"
);
let diag = parse(&src).unwrap();
let req = &diag.requirements[0];
assert_eq!(req.risk, Some(Risk::Medium));
assert_eq!(req.verify_method, Some(VerifyMethod::Inspection));
}
#[test]
fn missing_id_returns_error() {
let src = format!("{HEADER}requirement r1 {{\n text: some text.\n}}");
let err = parse(&src).unwrap_err();
assert!(
err.to_string().contains("id"),
"error message should mention missing id: {err}"
);
}
#[test]
fn missing_text_returns_error() {
let src = format!("{HEADER}requirement r1 {{\n id: 1\n}}");
let err = parse(&src).unwrap_err();
assert!(
err.to_string().contains("text"),
"error message should mention missing text: {err}"
);
}
#[test]
fn comment_lines_skipped() {
let src = format!(
"%% preamble\n{HEADER}\
%% inner comment\n\
requirement r1 {{\n\
id: 1\n\
text: some text. %% trailing\n\
}}"
);
let diag = parse(&src).unwrap();
assert_eq!(diag.requirements.len(), 1);
}
#[test]
fn malformed_relationship_arrow_returns_error() {
let src = format!("{HEADER}a - satisfies b");
let err = parse(&src).unwrap_err();
assert!(
err.to_string().contains("malformed"),
"expected malformed error, got: {err}"
);
}
#[test]
fn parses_canonical_example() {
let src = "requirementDiagram
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
functionalRequirement test_req2 {
id: 1.1
text: the second test text.
risk: low
verifymethod: inspection
}
element test_entity {
type: simulation
}
element test_entity2 {
type: word doc
docref: reqs/test_entity
}
test_entity - satisfies -> test_req2
test_req - traces -> test_req2
test_req - contains -> test_req";
let diag = parse(src).unwrap();
assert_eq!(diag.requirements.len(), 2);
assert_eq!(diag.elements.len(), 2);
assert_eq!(diag.relationships.len(), 3);
let req = &diag.requirements[0];
assert_eq!(req.name, "test_req");
assert_eq!(req.id, "1");
assert_eq!(req.risk, Some(Risk::High));
assert_eq!(req.verify_method, Some(VerifyMethod::Test));
let elem = &diag.elements[1];
assert_eq!(elem.kind, "word doc");
assert_eq!(elem.docref, Some("reqs/test_entity".to_string()));
assert_eq!(diag.relationships[0].kind, RelationshipKind::Satisfies);
assert_eq!(diag.relationships[0].source, "test_entity");
assert_eq!(diag.relationships[0].target, "test_req2");
}
#[test]
fn parses_all_verify_methods() {
for (kw, expected) in &[
("analysis", VerifyMethod::Analysis),
("inspection", VerifyMethod::Inspection),
("test", VerifyMethod::Test),
("demonstration", VerifyMethod::Demonstration),
] {
let src = format!(
"{HEADER}requirement r1 {{\n id: 1\n text: t.\n verifymethod: {kw}\n}}"
);
let diag = parse(&src).unwrap();
assert_eq!(
diag.requirements[0].verify_method,
Some(*expected),
"failed for verifymethod={kw}"
);
}
}
}