1#![forbid(unsafe_code)]
64#![allow(dead_code)]
65#![allow(clippy::missing_errors_doc)]
66
67use std::fmt;
68
69use crate::error::{GraphError, GraphResult};
70
71#[derive(Debug, Clone, PartialEq)]
79pub struct GraphDescription {
80 pub nodes: Vec<NodeSpec>,
82 pub edges: Vec<EdgeSpec>,
84}
85
86impl GraphDescription {
87 #[must_use]
89 pub fn is_empty(&self) -> bool {
90 self.nodes.is_empty()
91 }
92
93 #[must_use]
95 pub fn contains_node(&self, label: &str) -> bool {
96 self.nodes.iter().any(|n| n.label == label)
97 }
98
99 #[must_use]
101 pub fn node(&self, label: &str) -> Option<&NodeSpec> {
102 self.nodes.iter().find(|n| n.label == label)
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub struct NodeSpec {
109 pub label: String,
115 pub filter: String,
117 pub args: Vec<String>,
119}
120
121impl NodeSpec {
122 #[must_use]
124 pub fn new(label: impl Into<String>, filter: impl Into<String>) -> Self {
125 Self {
126 label: label.into(),
127 filter: filter.into(),
128 args: Vec::new(),
129 }
130 }
131
132 #[must_use]
134 pub fn with_args(
135 label: impl Into<String>,
136 filter: impl Into<String>,
137 args: Vec<String>,
138 ) -> Self {
139 Self {
140 label: label.into(),
141 filter: filter.into(),
142 args,
143 }
144 }
145}
146
147impl fmt::Display for NodeSpec {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}:{}", self.label, self.filter)?;
150 if !self.args.is_empty() {
151 write!(f, "({})", self.args.join(", "))?;
152 }
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct EdgeSpec {
160 pub from: String,
162 pub to: String,
164}
165
166impl EdgeSpec {
167 #[must_use]
169 pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
170 Self {
171 from: from.into(),
172 to: to.into(),
173 }
174 }
175}
176
177impl fmt::Display for EdgeSpec {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "{} -> {}", self.from, self.to)
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct ParseError {
190 pub message: String,
192 pub line: Option<usize>,
194 pub column: Option<usize>,
196}
197
198impl ParseError {
199 fn at(line: usize, column: usize, message: impl Into<String>) -> Self {
200 Self {
201 message: message.into(),
202 line: Some(line),
203 column: Some(column),
204 }
205 }
206
207 fn simple(message: impl Into<String>) -> Self {
208 Self {
209 message: message.into(),
210 line: None,
211 column: None,
212 }
213 }
214}
215
216impl fmt::Display for ParseError {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match (self.line, self.column) {
219 (Some(l), Some(c)) => write!(f, "parse error at {}:{}: {}", l, c, self.message),
220 (Some(l), None) => write!(f, "parse error at line {}: {}", l, self.message),
221 _ => write!(f, "parse error: {}", self.message),
222 }
223 }
224}
225
226impl From<ParseError> for GraphError {
227 fn from(e: ParseError) -> Self {
228 GraphError::ConfigurationError(e.to_string())
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Hash)]
238enum TokenKind {
239 Ident(String),
241 Quoted(String),
243 Arrow,
245 LParen,
247 RParen,
249 Comma,
251 Colon,
253 ChainSep,
255}
256
257#[derive(Debug, Clone)]
258struct Token {
259 kind: TokenKind,
260 line: usize,
261 col: usize,
262}
263
264fn tokenise(input: &str) -> Result<Vec<Token>, ParseError> {
269 let mut tokens = Vec::new();
270 let mut chars = input.char_indices().peekable();
271 let mut line = 1usize;
272 let mut line_start = 0usize;
273
274 while let Some(&(idx, ch)) = chars.peek() {
275 let col = idx - line_start + 1;
276
277 match ch {
278 ' ' | '\t' | '\r' => {
280 chars.next();
281 }
282
283 '\n' => {
285 chars.next();
286 if !tokens.is_empty() {
289 let last_is_sep = matches!(
291 tokens.last().map(|t: &Token| &t.kind),
292 Some(TokenKind::ChainSep)
293 );
294 if !last_is_sep {
295 tokens.push(Token {
296 kind: TokenKind::ChainSep,
297 line,
298 col,
299 });
300 }
301 }
302 line += 1;
303 line_start = idx + 1;
304 }
305
306 '#' => {
308 while let Some(&(_, c)) = chars.peek() {
309 if c == '\n' {
310 break;
311 }
312 chars.next();
313 }
314 }
315
316 '-' => {
318 chars.next();
319 match chars.peek() {
320 Some(&(_, '>')) => {
321 chars.next();
322 tokens.push(Token {
323 kind: TokenKind::Arrow,
324 line,
325 col,
326 });
327 }
328 Some(&(_, c)) if c.is_ascii_digit() => {
330 let mut num = String::from('-');
331 while let Some(&(_, c)) = chars.peek() {
332 if c.is_ascii_alphanumeric() || c == '.' || c == '_' {
333 num.push(c);
334 chars.next();
335 } else {
336 break;
337 }
338 }
339 tokens.push(Token {
340 kind: TokenKind::Ident(num),
341 line,
342 col,
343 });
344 }
345 _ => {
346 return Err(ParseError::at(
347 line,
348 col,
349 "unexpected '-'; did you mean '->'?",
350 ));
351 }
352 }
353 }
354
355 '(' => {
356 chars.next();
357 tokens.push(Token {
358 kind: TokenKind::LParen,
359 line,
360 col,
361 });
362 }
363 ')' => {
364 chars.next();
365 tokens.push(Token {
366 kind: TokenKind::RParen,
367 line,
368 col,
369 });
370 }
371 ',' => {
372 chars.next();
373 tokens.push(Token {
374 kind: TokenKind::Comma,
375 line,
376 col,
377 });
378 }
379 ':' => {
380 chars.next();
381 tokens.push(Token {
382 kind: TokenKind::Colon,
383 line,
384 col,
385 });
386 }
387 ';' => {
388 chars.next();
389 let last_is_sep = matches!(
390 tokens.last().map(|t: &Token| &t.kind),
391 Some(TokenKind::ChainSep)
392 );
393 if !last_is_sep {
394 tokens.push(Token {
395 kind: TokenKind::ChainSep,
396 line,
397 col,
398 });
399 }
400 }
401
402 '"' => {
404 chars.next();
405 let mut s = String::new();
406 let mut closed = false;
407 while let Some(&(_, c)) = chars.peek() {
408 chars.next();
409 if c == '"' {
410 closed = true;
411 break;
412 }
413 if c == '\\' {
414 if let Some(&(_, escaped)) = chars.peek() {
416 chars.next();
417 match escaped {
418 'n' => s.push('\n'),
419 't' => s.push('\t'),
420 '"' => s.push('"'),
421 '\\' => s.push('\\'),
422 other => {
423 s.push('\\');
424 s.push(other);
425 }
426 }
427 }
428 } else {
429 s.push(c);
430 }
431 }
432 if !closed {
433 return Err(ParseError::at(line, col, "unterminated string literal"));
434 }
435 tokens.push(Token {
436 kind: TokenKind::Quoted(s),
437 line,
438 col,
439 });
440 }
441
442 c if c.is_ascii_alphanumeric() || c == '_' => {
444 let mut ident = String::new();
445 while let Some(&(_, c)) = chars.peek() {
446 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
447 if c == '-' {
449 let rest: String = {
451 let mut tmp = chars.clone();
452 tmp.next(); tmp.peek().map(|&(_, x)| x).map_or(String::new(), |x| {
454 let mut s = String::from('-');
455 s.push(x);
456 s
457 })
458 };
459 if rest == "->" {
460 break;
461 }
462 }
463 ident.push(c);
464 chars.next();
465 } else {
466 break;
467 }
468 }
469 tokens.push(Token {
470 kind: TokenKind::Ident(ident),
471 line,
472 col,
473 });
474 }
475
476 other => {
478 return Err(ParseError::at(
479 line,
480 col,
481 format!("unexpected character '{other}'"),
482 ));
483 }
484 }
485 }
486
487 Ok(tokens)
488}
489
490struct Parser {
495 tokens: Vec<Token>,
496 pos: usize,
497 counters: std::collections::HashMap<String, usize>,
499}
500
501impl Parser {
502 fn new(tokens: Vec<Token>) -> Self {
503 Self {
504 tokens,
505 pos: 0,
506 counters: std::collections::HashMap::new(),
507 }
508 }
509
510 fn peek(&self) -> Option<&Token> {
511 self.tokens.get(self.pos)
512 }
513
514 fn next_token(&mut self) -> Option<&Token> {
515 let t = self.tokens.get(self.pos);
516 self.pos += 1;
517 t
518 }
519
520 fn skip_seps(&mut self) -> bool {
522 let mut skipped = false;
523 while matches!(self.peek().map(|t| &t.kind), Some(TokenKind::ChainSep)) {
524 self.pos += 1;
525 skipped = true;
526 }
527 skipped
528 }
529
530 fn auto_label(&mut self, filter_name: &str) -> String {
532 let count = self.counters.entry(filter_name.to_owned()).or_insert(0);
533 let label = format!("{}_{}", filter_name, count);
534 *count += 1;
535 label
536 }
537
538 fn parse_node_spec(&mut self) -> Result<NodeSpec, ParseError> {
540 let label_or_filter = match self.peek() {
542 Some(Token {
543 kind: TokenKind::Ident(s),
544 ..
545 }) => s.clone(),
546 Some(t) => {
547 return Err(ParseError::at(
548 t.line,
549 t.col,
550 format!("expected node name, found {:?}", t.kind),
551 ));
552 }
553 None => {
554 return Err(ParseError::simple("unexpected end of input in node spec"));
555 }
556 };
557 self.pos += 1; let (label, filter) = if matches!(self.peek().map(|t| &t.kind), Some(TokenKind::Colon)) {
561 self.pos += 1; let filter = match self.peek() {
563 Some(Token {
564 kind: TokenKind::Ident(s),
565 ..
566 }) => s.clone(),
567 Some(t) => {
568 return Err(ParseError::at(
569 t.line,
570 t.col,
571 "expected filter name after ':'",
572 ));
573 }
574 None => {
575 return Err(ParseError::simple("expected filter name after ':'"));
576 }
577 };
578 self.pos += 1; (label_or_filter, filter)
580 } else {
581 let filter = label_or_filter.clone();
583 let label = self.auto_label(&filter);
584 (label, filter)
585 };
586
587 let args = if matches!(self.peek().map(|t| &t.kind), Some(TokenKind::LParen)) {
589 self.pos += 1; self.parse_args()?
591 } else {
592 Vec::new()
593 };
594
595 Ok(NodeSpec {
596 label,
597 filter,
598 args,
599 })
600 }
601
602 fn parse_args(&mut self) -> Result<Vec<String>, ParseError> {
604 let mut args = Vec::new();
605 loop {
606 let kind_opt = self.peek().map(|t| t.kind.clone());
608 match kind_opt {
609 Some(TokenKind::RParen) => {
610 self.pos += 1; break;
612 }
613 Some(TokenKind::Ident(s)) => {
614 args.push(s);
615 self.pos += 1;
616 }
617 Some(TokenKind::Quoted(s)) => {
618 args.push(s);
619 self.pos += 1;
620 }
621 Some(TokenKind::Comma) => {
622 self.pos += 1; }
624 _ => {
625 if let Some(t) = self.peek() {
626 return Err(ParseError::at(
627 t.line,
628 t.col,
629 format!("unexpected token in argument list: {:?}", t.kind),
630 ));
631 }
632 return Err(ParseError::simple("unterminated argument list"));
633 }
634 }
635 }
636 Ok(args)
637 }
638
639 fn parse_chain(
641 &mut self,
642 nodes: &mut Vec<NodeSpec>,
643 edges: &mut Vec<EdgeSpec>,
644 ) -> Result<(), ParseError> {
645 let first = self.parse_node_spec()?;
646 let mut prev_label = first.label.clone();
647 if !nodes.iter().any(|n| n.label == first.label) {
648 nodes.push(first);
649 }
650
651 loop {
652 let kind_opt = self.peek().map(|t| t.kind.clone());
654 match kind_opt {
655 Some(TokenKind::Arrow) => {
656 self.pos += 1; let next = self.parse_node_spec()?;
658 let next_label = next.label.clone();
659 edges.push(EdgeSpec::new(prev_label.clone(), next_label.clone()));
661 if !nodes.iter().any(|n| n.label == next.label) {
663 nodes.push(next);
664 }
665 prev_label = next_label;
666 }
667 Some(TokenKind::ChainSep) | None => {
669 break;
670 }
671 _ => {
672 if let Some(t) = self.peek() {
673 return Err(ParseError::at(
674 t.line,
675 t.col,
676 format!("expected '->' or end of chain, found {:?}", t.kind),
677 ));
678 }
679 break;
680 }
681 }
682 }
683 Ok(())
684 }
685
686 fn parse(mut self) -> Result<GraphDescription, ParseError> {
688 let mut nodes: Vec<NodeSpec> = Vec::new();
689 let mut edges: Vec<EdgeSpec> = Vec::new();
690
691 self.skip_seps();
693
694 while self.peek().is_some() {
695 self.parse_chain(&mut nodes, &mut edges)?;
696 self.skip_seps();
697 }
698
699 Ok(GraphDescription { nodes, edges })
700 }
701}
702
703pub fn parse_graph_dsl(input: &str) -> GraphResult<GraphDescription> {
730 let tokens = tokenise(input).map_err(GraphError::from)?;
731 let parser = Parser::new(tokens);
732 parser.parse().map_err(GraphError::from)
733}
734
735#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
746 fn test_parse_simple_chain() {
747 let dsl = "source -> scale(1920,1080) -> encoder -> sink";
748 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
749 assert_eq!(desc.nodes.len(), 4);
750 assert_eq!(desc.edges.len(), 3);
751 }
752
753 #[test]
754 fn test_parse_single_node() {
755 let desc = parse_graph_dsl("source").expect("parse should succeed");
756 assert_eq!(desc.nodes.len(), 1);
757 assert_eq!(desc.edges.len(), 0);
758 assert_eq!(desc.nodes[0].filter, "source");
759 }
760
761 #[test]
762 fn test_parse_node_with_args() {
763 let desc = parse_graph_dsl("scale(1280,720)").expect("parse should succeed");
764 assert_eq!(desc.nodes[0].filter, "scale");
765 assert_eq!(desc.nodes[0].args, vec!["1280", "720"]);
766 }
767
768 #[test]
769 fn test_parse_explicit_label() {
770 let desc = parse_graph_dsl("my_src:source -> sink").expect("parse should succeed");
771 assert_eq!(desc.nodes[0].label, "my_src");
772 assert_eq!(desc.nodes[0].filter, "source");
773 }
774
775 #[test]
778 fn test_edges_connect_sequential_nodes() {
779 let desc = parse_graph_dsl("a -> b -> c").expect("parse should succeed");
780 assert_eq!(desc.edges.len(), 2);
781 assert_eq!(desc.edges[0].from, desc.nodes[0].label);
782 assert_eq!(desc.edges[0].to, desc.nodes[1].label);
783 assert_eq!(desc.edges[1].from, desc.nodes[1].label);
784 assert_eq!(desc.edges[1].to, desc.nodes[2].label);
785 }
786
787 #[test]
790 fn test_parse_multiline_chains() {
791 let dsl = "src -> filter_a\nfilter_b -> sink";
792 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
793 assert_eq!(desc.nodes.len(), 4);
795 assert_eq!(desc.edges.len(), 2);
796 }
797
798 #[test]
799 fn test_parse_semicolon_separated_chains() {
800 let dsl = "src -> enc; src2 -> enc2";
801 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
802 assert_eq!(desc.nodes.len(), 4);
803 assert_eq!(desc.edges.len(), 2);
804 }
805
806 #[test]
809 fn test_shared_node_deduplication() {
810 let dsl = "tee:split\ntee -> branch_a\ntee -> branch_b";
811 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
812 let tee_count = desc.nodes.iter().filter(|n| n.label == "tee").count();
814 assert_eq!(tee_count, 1, "shared node must be deduplicated");
815 }
816
817 #[test]
820 fn test_comments_are_ignored() {
821 let dsl = "# this is a comment\nsource -> sink\n# another comment";
822 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
823 assert_eq!(desc.nodes.len(), 2);
824 }
825
826 #[test]
829 fn test_empty_input() {
830 let desc = parse_graph_dsl("").expect("parse should succeed");
831 assert!(desc.is_empty());
832 }
833
834 #[test]
835 fn test_whitespace_only_input() {
836 let desc = parse_graph_dsl(" \n \t ").expect("parse should succeed");
837 assert!(desc.is_empty());
838 }
839
840 #[test]
843 fn test_quoted_args_preserve_spaces() {
844 let desc =
845 parse_graph_dsl(r#"watermark("hello world",50,50)"#).expect("parse should succeed");
846 assert_eq!(desc.nodes[0].args[0], "hello world");
847 }
848
849 #[test]
852 fn test_contains_node() {
853 let desc = parse_graph_dsl("src -> sink").expect("parse should succeed");
854 assert!(desc.contains_node("src_0"));
856 assert!(!desc.contains_node("nonexistent"));
857 }
858
859 #[test]
860 fn test_node_lookup() {
861 let desc = parse_graph_dsl("my:scale(1920,1080)").expect("parse should succeed");
862 let node = desc.node("my").expect("node should exist");
863 assert_eq!(node.filter, "scale");
864 assert_eq!(node.args, vec!["1920", "1080"]);
865 }
866
867 #[test]
870 fn test_node_spec_display_no_args() {
871 let n = NodeSpec::new("my_src", "source");
872 assert_eq!(n.to_string(), "my_src:source");
873 }
874
875 #[test]
876 fn test_node_spec_display_with_args() {
877 let n = NodeSpec::with_args("s0", "scale", vec!["1920".into(), "1080".into()]);
878 assert_eq!(n.to_string(), "s0:scale(1920, 1080)");
879 }
880
881 #[test]
882 fn test_edge_spec_display() {
883 let e = EdgeSpec::new("src", "sink");
884 assert_eq!(e.to_string(), "src -> sink");
885 }
886
887 #[test]
890 fn test_unterminated_string_returns_error() {
891 let result = parse_graph_dsl(r#"node("unterminated)"#);
892 assert!(result.is_err());
893 }
894
895 #[test]
896 fn test_unexpected_char_returns_error() {
897 let result = parse_graph_dsl("node @ other");
898 assert!(result.is_err());
899 }
900
901 #[test]
902 fn test_bare_arrow_returns_error() {
903 let result = parse_graph_dsl("-> sink");
904 assert!(result.is_err());
905 }
906
907 #[test]
910 fn test_complex_pipeline() {
911 let dsl = r#"
912 # Full transcode pipeline
913 input:source -> deinterlace -> normalize(loudness,-14)
914 input -> scale(1920,1080) -> h264:encoder(crf,23) -> mux:mp4_sink
915 "#;
916 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
917 assert!(desc.nodes.len() >= 4);
919 assert!(desc.edges.len() >= 3);
920 let input_count = desc.nodes.iter().filter(|n| n.label == "input").count();
922 assert_eq!(input_count, 1, "shared 'input' node must be deduplicated");
923 }
924
925 #[test]
926 fn test_auto_label_uniqueness() {
927 let dsl = "src -> scale(1920,1080)\nsrc2 -> scale(640,360)";
929 let desc = parse_graph_dsl(dsl).expect("parse should succeed");
930 let scale_labels: Vec<&str> = desc
931 .nodes
932 .iter()
933 .filter(|n| n.filter == "scale")
934 .map(|n| n.label.as_str())
935 .collect();
936 assert_eq!(scale_labels.len(), 2);
937 assert_ne!(
938 scale_labels[0], scale_labels[1],
939 "auto-labels must be unique"
940 );
941 }
942}