argumentation/parsers/
apx.rs1use crate::Error;
14use crate::framework::ArgumentationFramework;
15
16pub fn parse_apx(input: &str) -> Result<ArgumentationFramework<String>, Error> {
18 let mut af = ArgumentationFramework::new();
19 for (lineno, raw_line) in input.lines().enumerate() {
20 let line = raw_line.split('%').next().unwrap_or("").trim();
21 if line.is_empty() {
22 continue;
23 }
24 if let Some(rest) = line.strip_prefix("arg(") {
25 let body = extract_paren_body(rest, lineno)?;
26 if body.is_empty() {
27 return Err(Error::Parse(format!("line {}: empty arg", lineno + 1)));
28 }
29 af.add_argument(body.to_string());
30 } else if let Some(rest) = line.strip_prefix("att(") {
31 let body = extract_paren_body(rest, lineno)?;
32 let parts: Vec<&str> = body.split(',').map(str::trim).collect();
33 if parts.len() != 2 {
34 return Err(Error::Parse(format!(
35 "line {}: att expects 2 args, got {}",
36 lineno + 1,
37 parts.len()
38 )));
39 }
40 af.add_attack(&parts[0].to_string(), &parts[1].to_string())
41 .map_err(|e| Error::Parse(format!("line {}: {}", lineno + 1, e)))?;
42 } else {
43 return Err(Error::Parse(format!(
44 "line {}: unrecognised: {}",
45 lineno + 1,
46 line
47 )));
48 }
49 }
50 Ok(af)
51}
52
53fn extract_paren_body(rest: &str, lineno: usize) -> Result<&str, Error> {
57 let Some(close_idx) = rest.rfind(')') else {
58 return Err(Error::Parse(format!(
59 "line {}: missing closing `)`",
60 lineno + 1
61 )));
62 };
63 let (body, tail) = rest.split_at(close_idx);
64 let tail = &tail[1..]; if !tail.is_empty() && tail != "." {
66 return Err(Error::Parse(format!(
67 "line {}: unexpected text after `)`: {:?}",
68 lineno + 1,
69 tail
70 )));
71 }
72 Ok(body.trim())
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn parse_simple_apx() {
81 let input = "arg(a).\narg(b).\natt(a,b).\n";
82 let af = parse_apx(input).unwrap();
83 assert_eq!(af.arguments().count(), 2);
84 assert_eq!(af.attackers(&"b".to_string()).len(), 1);
85 }
86
87 #[test]
88 fn parse_apx_with_comments() {
89 let input = "% test\narg(x).\narg(y).\n% comment\natt(x, y).\n";
90 let af = parse_apx(input).unwrap();
91 assert_eq!(af.arguments().count(), 2);
92 }
93
94 #[test]
95 fn parse_apx_rejects_unknown_syntax() {
96 let input = "foo(a).\n";
97 assert!(parse_apx(input).is_err());
98 }
99
100 #[test]
101 fn parse_apx_rejects_tab_between_paren_and_period() {
102 let input = "arg(a)\t.\n";
104 let err = parse_apx(input).unwrap_err();
105 assert!(
106 matches!(err, Error::Parse(_)),
107 "expected Parse error, got {:?}",
108 err
109 );
110 }
111
112 #[test]
113 fn parse_apx_rejects_trailing_garbage_after_close() {
114 let input = "arg(a)extra.\n";
115 assert!(parse_apx(input).is_err());
116 }
117
118 #[test]
119 fn parse_apx_rejects_missing_close_paren() {
120 let input = "arg(a.\n";
121 assert!(parse_apx(input).is_err());
122 }
123
124 #[test]
125 fn parse_apx_accepts_extra_whitespace_inside() {
126 let input = "arg( a ).\narg( b ).\natt( a , b ).\n";
128 let af = parse_apx(input).unwrap();
129 assert_eq!(af.arguments().count(), 2);
130 assert_eq!(af.attackers(&"b".to_string()).len(), 1);
131 }
132}