haddock_restraints/core/
tbl_parser.rs1#[derive(Debug, Clone, PartialEq)]
10pub struct ResSelection {
11 pub resid: i16,
12 pub chain: String,
13}
14
15#[derive(Debug, Clone, PartialEq)]
28pub struct ParsedRestraint {
29 pub active: ResSelection,
30 pub partners: Vec<ResSelection>,
31}
32
33pub fn parse_tbl(content: &str) -> Result<Vec<ParsedRestraint>, String> {
46 let cleaned = strip_comments(content);
47 let mut restraints = Vec::new();
48 let mut saw_assign_block = false;
49
50 for chunk in cleaned.split("assign").skip(1) {
51 saw_assign_block = true;
52
53 let mut selections = extract_selections(chunk)?.into_iter();
54
55 let Some(active) = selections.next() else {
56 continue;
57 };
58
59 restraints.push(ParsedRestraint {
60 active,
61 partners: selections.collect(),
62 });
63 }
64
65 if saw_assign_block && restraints.is_empty() {
66 return Err(
67 "no resid/segid restraints found in .tbl (only unsupported restraint types?)"
68 .to_string(),
69 );
70 }
71
72 Ok(restraints)
73}
74
75fn strip_comments(content: &str) -> String {
77 content
78 .lines()
79 .map(|line| line.split('!').next().unwrap_or(""))
80 .collect::<Vec<_>>()
81 .join("\n")
82}
83
84fn tokenize(chunk: &str) -> Vec<&str> {
88 let mut tokens = Vec::new();
89 let mut chars = chunk.char_indices().peekable();
90
91 while let Some(&(start, c)) = chars.peek() {
92 if c.is_whitespace() {
93 chars.next();
94 continue;
95 }
96 if c == '(' || c == ')' {
97 tokens.push(&chunk[start..start + c.len_utf8()]);
98 chars.next();
99 continue;
100 }
101 let mut end = start + c.len_utf8();
102 chars.next();
103 while let Some(&(idx, c2)) = chars.peek() {
104 if c2.is_whitespace() || c2 == '(' || c2 == ')' {
105 break;
106 }
107 end = idx + c2.len_utf8();
108 chars.next();
109 }
110 tokens.push(&chunk[start..end]);
111 }
112
113 tokens
114}
115
116fn extract_selections(chunk: &str) -> Result<Vec<ResSelection>, String> {
129 let tokens = tokenize(chunk);
130
131 let mut stack: Vec<(usize, bool)> = Vec::new();
133 let mut selections = Vec::new();
134
135 for (idx, &tok) in tokens.iter().enumerate() {
136 match tok {
137 "(" => {
138 if let Some(parent) = stack.last_mut() {
139 parent.1 = true;
140 }
141 stack.push((idx + 1, false));
142 }
143 ")" => {
144 if let Some((start, has_nested)) = stack.pop()
145 && !has_nested
146 && let Some(selection) = parse_selection(&tokens[start..idx])?
147 {
148 selections.push(selection);
149 }
150 }
151 _ => {}
152 }
153 }
154
155 Ok(selections)
156}
157
158fn parse_selection(tokens: &[&str]) -> Result<Option<ResSelection>, String> {
162 let mut resid = None;
163 let mut chain = None;
164
165 let mut i = 0;
166 while i < tokens.len() {
167 match tokens[i] {
168 "resid" => {
169 let value = tokens.get(i + 1).ok_or_else(|| {
170 "\"resid\" keyword with no residue number after it".to_string()
171 })?;
172 resid = Some(
173 value
174 .parse::<i16>()
175 .map_err(|_| format!("invalid residue number \"{}\"", value))?,
176 );
177 i += 1;
178 }
179 "segid" => {
180 let value = tokens.get(i + 1).ok_or_else(|| {
181 "\"segid\" keyword with no chain identifier after it".to_string()
182 })?;
183 chain = Some((*value).to_string());
184 i += 1;
185 }
186 _ => {}
187 }
188 i += 1;
189 }
190
191 Ok(resid
192 .zip(chain)
193 .map(|(resid, chain)| ResSelection { resid, chain }))
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_parse_oneline() {
202 let tbl = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
203 let restraints = parse_tbl(tbl).unwrap();
204
205 assert_eq!(
206 restraints,
207 vec![ParsedRestraint {
208 active: ResSelection {
209 resid: 1,
210 chain: "A".to_string()
211 },
212 partners: vec![ResSelection {
213 resid: 2,
214 chain: "B".to_string()
215 }],
216 }]
217 );
218 }
219
220 #[test]
221 fn test_parse_multiline_or() {
222 let tbl = "assign ( resid 1 and segid A )\n (\n ( resid 2 and segid B )\n or\n ( resid 3 and segid B )\n ) 2.0 2.0 0.0\n\n";
223 let restraints = parse_tbl(tbl).unwrap();
224
225 assert_eq!(
226 restraints,
227 vec![ParsedRestraint {
228 active: ResSelection {
229 resid: 1,
230 chain: "A".to_string()
231 },
232 partners: vec![
233 ResSelection {
234 resid: 2,
235 chain: "B".to_string()
236 },
237 ResSelection {
238 resid: 3,
239 chain: "B".to_string()
240 },
241 ],
242 }]
243 );
244 }
245
246 #[test]
247 fn test_parse_ignores_atom_clauses() {
248 let tbl = "assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B and name CB ) 2.0 2.0 0.0\n\n";
249 let restraints = parse_tbl(tbl).unwrap();
250
251 assert_eq!(restraints[0].active.resid, 1);
252 assert_eq!(restraints[0].active.chain, "A");
253 assert_eq!(restraints[0].partners[0].resid, 2);
254 assert_eq!(restraints[0].partners[0].chain, "B");
255 }
256
257 #[test]
258 fn test_parse_multiple_blocks() {
259 let tbl = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n\
260 assign ( resid 5 and segid A ) ( resid 9 and segid B ) 2.0 2.0 0.0\n\n";
261 let restraints = parse_tbl(tbl).unwrap();
262
263 assert_eq!(restraints.len(), 2);
264 assert_eq!(restraints[1].active.resid, 5);
265 }
266
267 #[test]
268 fn test_parse_ignores_comments() {
269 let tbl = "! this is a comment\nassign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0 ! inline comment\n\n";
270 let restraints = parse_tbl(tbl).unwrap();
271
272 assert_eq!(restraints[0].active.resid, 1);
273 }
274
275 #[test]
276 fn test_parse_atom_clause_before_segid() {
277 let tbl =
279 "assign ( resid 1 and name CA and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
280 let restraints = parse_tbl(tbl).unwrap();
281
282 assert_eq!(restraints[0].active.resid, 1);
283 assert_eq!(restraints[0].active.chain, "A");
284 }
285
286 #[test]
287 fn test_parse_segid_before_resid() {
288 let tbl = "assign ( segid A and resid 1 ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
289 let restraints = parse_tbl(tbl).unwrap();
290
291 assert_eq!(restraints[0].active.resid, 1);
292 assert_eq!(restraints[0].active.chain, "A");
293 }
294
295 #[test]
296 fn test_parse_skips_non_air_block_without_erroring() {
297 let tbl = "assign ( attr store1 ) ( attr store2 ) 5.0 0.0 0.0\n\n\
302 assign ( resid 5 and segid A ) ( resid 9 and segid B ) 2.0 2.0 0.0\n\n";
303 let restraints = parse_tbl(tbl).unwrap();
304
305 assert_eq!(restraints.len(), 1);
306 assert_eq!(restraints[0].active.resid, 5);
307 }
308
309 #[test]
310 fn test_parse_invalid_resid_errors() {
311 let tbl = "assign ( resid 99999999 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
312 assert!(parse_tbl(tbl).is_err());
313 }
314
315 #[test]
316 fn test_parse_segid_without_chain_errors() {
317 let tbl = "assign ( resid 1 and segid ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
318 assert!(parse_tbl(tbl).is_err());
319 }
320
321 #[test]
322 fn test_parse_empty_file_yields_no_restraints() {
323 assert!(parse_tbl("").unwrap().is_empty());
324 assert!(parse_tbl("! just a comment\n").unwrap().is_empty());
325 }
326
327 #[test]
328 fn test_parse_only_non_air_blocks_errors() {
329 let tbl = "assign ( attr store1 ) ( attr store2 ) 5.0 0.0 0.0\n\n";
333 assert!(parse_tbl(tbl).is_err());
334 }
335
336 #[test]
337 fn test_extract_selections_no_surrounding_whitespace() {
338 let selections = extract_selections("(resid 1 and segid A)(resid 2 and segid B)").unwrap();
339 assert_eq!(
340 selections,
341 vec![
342 ResSelection {
343 resid: 1,
344 chain: "A".to_string()
345 },
346 ResSelection {
347 resid: 2,
348 chain: "B".to_string()
349 },
350 ]
351 );
352 }
353
354 #[test]
355 fn test_extract_selections() {
356 let selections =
357 extract_selections("( resid 1 and segid A ) ( resid 2 and segid B )").unwrap();
358 assert_eq!(
359 selections,
360 vec![
361 ResSelection {
362 resid: 1,
363 chain: "A".to_string()
364 },
365 ResSelection {
366 resid: 2,
367 chain: "B".to_string()
368 },
369 ]
370 );
371 }
372}