injm 0.5.1

A CLI tool that injects content into marked regions in source files.
use super::{InjectorError, Result};
use crate::types::{BlockRole, MarkerBlock};

pub fn inject(
    content: &str,
    output_blocks: &[MarkerBlock],
    input_blocks: &[MarkerBlock],
) -> Result<String> {
    let mut lines: Vec<&str> = content.lines().collect();

    // Use reversed iteration to avoid changes of line number.
    for block in output_blocks.iter().rev() {
        if let Some(input_block) = input_blocks.iter().find(|b| b.matches_output(block)) {
            match &input_block.role {
                BlockRole::Input { content, .. } => {
                    lines = inject_into_a_block(&lines, block, content)
                }
                _ => return Err(InjectorError::EmptyInputContent),
            }
        }
    }

    let mut result = lines.join("\n");
    if content.ends_with('\n') {
        result.push('\n');
    }

    Ok(result)
}

fn inject_into_a_block<'a>(lines: &[&'a str], block: &MarkerBlock, stdin: &'a str) -> Vec<&'a str> {
    // Content to be replaced is in (begin_line, end_line).
    let before = &lines[block.span.before_lines()];
    let after = &lines[block.span.after_lines()];

    let mut injected = Vec::new();
    injected.extend_from_slice(before);
    injected.push(stdin);
    injected.extend_from_slice(after);

    injected
}

#[cfg(test)]
mod tests {
    use std::vec;

    use super::*;
    use crate::types::{BlockRole, MarkerBlock, SourceSpan};

    fn make_default_input_blocks(s: &str) -> Vec<MarkerBlock> {
        vec![MarkerBlock {
            span: SourceSpan::new(0, 0),
            role: BlockRole::Input {
                ids: vec![],
                content: s.to_string(),
            },
        }]
    }

    #[test]
    fn test_inject_single_block() {
        let content = "fn main() {\n    // injm begin\n    old content\n    // injm end\n}\n";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(1, 3),
            role: BlockRole::Default,
        }];
        let result = inject(
            content,
            &blocks,
            &make_default_input_blocks("    new content"),
        )
        .unwrap();
        assert!(result.contains("new content"));
        assert!(!result.contains("old content"));
    }

    #[test]
    fn test_inject_preserves_markers() {
        let content = "// injm begin\nold\n// injm end\n";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 2),
            role: BlockRole::Default,
        }];
        let result = inject(content, &blocks, &make_default_input_blocks("new")).unwrap();
        assert!(result.contains("// injm begin"));
        assert!(result.contains("// injm end"));
    }

    #[test]
    fn test_inject_preserves_trailing_newline() {
        let content = "// injm begin\nold\n// injm end\n";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 2),
            role: BlockRole::Default,
        }];
        let result = inject(content, &blocks, &make_default_input_blocks("new")).unwrap();
        assert!(result.ends_with('\n'));
    }

    #[test]
    fn test_inject_no_trailing_newline() {
        let content = "// injm begin\nold\n// injm end";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 2),
            role: BlockRole::Default,
        }];
        let result = inject(content, &blocks, &make_default_input_blocks("new")).unwrap();
        assert!(!result.ends_with('\n'));
    }

    #[test]
    fn test_inject_multiple_blocks() {
        let content =
            "// injm begin\nold one\n// injm end\ncode\n// injm begin\nold two\n// injm end\n";
        let blocks = vec![
            MarkerBlock {
                span: SourceSpan::new(0, 2),
                role: BlockRole::Default,
            },
            MarkerBlock {
                span: SourceSpan::new(4, 6),
                role: BlockRole::Default,
            },
        ];
        let result = inject(content, &blocks, &make_default_input_blocks("new")).unwrap();
        assert!(!result.contains("old one"));
        assert!(!result.contains("old two"));
        assert_eq!(result.matches("new").count(), 2);
    }

    #[test]
    fn test_inject_empty_block() {
        let content = "// injm begin\n// injm end\n";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 1),
            role: BlockRole::Default,
        }];
        let result = inject(content, &blocks, &make_default_input_blocks("new content")).unwrap();
        assert!(result.contains("new content"));
    }

    #[test]
    fn test_inject_multiline_stdin() {
        let content = "// injm begin\nold\n// injm end\n";
        let blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 2),
            role: BlockRole::Default,
        }];
        let result = inject(
            content,
            &blocks,
            &make_default_input_blocks("line one\nline two\nline three"),
        )
        .unwrap();
        assert!(result.contains("line one\nline two\nline three"));
    }

    #[test]
    fn test_inject_with_id() {
        let content = "\
// injm begin <first
old first
// injm end 
// injm begin <second
old second
// injm end 
";
        let blocks = vec![
            MarkerBlock {
                span: SourceSpan::new(0, 2),
                role: BlockRole::Output {
                    id: Some("first".to_string()),
                },
            },
            MarkerBlock {
                span: SourceSpan::new(3, 5),
                role: BlockRole::Output {
                    id: Some("second".to_string()),
                },
            },
        ];

        let input_blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 0),
            role: BlockRole::Input {
                ids: vec!["first".to_string()],
                content: "new content".to_string(),
            },
        }];

        let result = inject(content, &blocks, &input_blocks).unwrap();
        assert!(result.contains("new content"));
        assert!(!result.contains("old first"));
        assert!(result.contains("old second"));
    }

    #[test]
    fn test_inject_when_no_ids() {
        let content = "\
// injm begin <first
old first
// injm end 
// injm begin <second
old second
// injm end 
// injm begin
// injm end
";
        let blocks = vec![
            MarkerBlock {
                span: SourceSpan::new(0, 2),
                role: BlockRole::Output {
                    id: Some("first".to_string()),
                },
            },
            MarkerBlock {
                span: SourceSpan::new(3, 5),
                role: BlockRole::Output {
                    id: Some("second".to_string()),
                },
            },
            MarkerBlock {
                span: SourceSpan::new(6, 8),
                role: BlockRole::Default,
            },
        ];
        let result = inject(content, &blocks, &make_default_input_blocks("new content")).unwrap();
        assert!(result.contains("old first"));
        assert!(result.contains("old second"));
        assert_eq!(result.matches("new content").count(), 1);
    }

    #[test]
    fn test_inject_multiple_ids() {
        let content = "\
// injm begin <first
old first
// injm end 
// injm begin <second
old second
// injm end 
// injm begin <third
old third
// injm end 
";
        let blocks = vec![
            MarkerBlock {
                span: SourceSpan::new(0, 2),
                role: BlockRole::Output {
                    id: Some("first".to_string()),
                },
            },
            MarkerBlock {
                span: SourceSpan::new(3, 5),
                role: BlockRole::Output {
                    id: Some("second".to_string()),
                },
            },
            MarkerBlock {
                span: SourceSpan::new(6, 8),
                role: BlockRole::Output {
                    id: Some("third".to_string()),
                },
            },
        ];

        let input_blocks = vec![MarkerBlock {
            span: SourceSpan::new(0, 0),
            role: BlockRole::Input {
                ids: vec!["first".to_string(), "third".to_string()],
                content: "new content".to_string(),
            },
        }];

        let result = inject(content, &blocks, &input_blocks).unwrap();
        assert!(!result.contains("old first"));
        assert!(result.contains("old second"));
        assert!(!result.contains("old third"));
        assert_eq!(result.matches("new content").count(), 2);
    }
}