use yaml_rust2::parser::Parser; use yaml_rust2::Event; use std::ops::Range; enum YContext { InDocument, InObject, InSequence, InValue(bool), } #[derive(Debug, PartialEq)] pub struct ManifestFile { pub image_tags: Vec>, } impl ManifestFile { pub fn parse(content: &str) -> Self { let mut yaml = Parser::new_from_str(content); let mut images = vec!(); let mut scope = vec!(); loop { let (ev, mark) = yaml.next_token().unwrap(); match ev { Event::StreamStart => {} Event::StreamEnd => { break; } Event::DocumentStart => { scope.push(YContext::InDocument); } Event::DocumentEnd => { assert!(matches!(scope.pop().unwrap(), YContext::InDocument)); scope.pop_if(|x| matches!(x, YContext::InValue(_))); }, Event::MappingStart(_, _) => { scope.push(YContext::InObject); }, Event::MappingEnd => { assert!(matches!(scope.pop().unwrap(), YContext::InObject)); scope.pop_if(|x| matches!(x, YContext::InValue(_))); }, Event::SequenceStart(_, _) => { scope.push(YContext::InSequence); }, Event::SequenceEnd => { assert!(matches!(scope.pop().unwrap(), YContext::InSequence)); scope.pop_if(|x| matches!(x, YContext::InValue(_))); }, Event::Scalar(ref txt, _, _, _) => { let parent = scope.last().unwrap(); match parent { YContext::InObject => { // We are the key of a mapping, which means the next even is the value scope.push(YContext::InValue(txt == "image")); }, YContext::InSequence => {}, YContext::InValue(img) => { if *img { images.push(mark.index()..mark.index() + txt.len()); } scope.pop(); }, // This should only happen for entirely empty documents YContext::InDocument => assert!(txt == ""), } }, x => todo!("{:?}", x), } } return Self{ image_tags: images, }; } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_single_image() { let yaml = "image: nginx:1.21"; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 1); assert_eq!(manifest.image_tags[0], 7..17); } #[test] fn parse_multiple_images() { let yaml = r#" containers: - image: app:v1 - image: sidecar:v2 "#; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 2); assert_eq!(manifest.image_tags[0], 22..28); assert_eq!(manifest.image_tags[1], 38..48); } #[test] fn parse_no_images() { let yaml = "key: value"; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 0); } #[test] fn parse_empty_document() { let yaml = ""; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 0); } #[test] fn parse_nested_object() { let yaml = r#" spec: container: image: nginx:1.21 "#; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 1); assert_eq!(manifest.image_tags[0], 31..41); } #[test] fn parse_multiple_documents() { let yaml = r#" kind: Service --- image: nginx:1.21 "#; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 1); assert_eq!(manifest.image_tags[0], 26..36); } #[test] fn parse_image_with_digest() { let yaml = "image: nginx@sha256:abc123"; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 1); assert_eq!(manifest.image_tags[0], 7..26); } #[test] fn parse_image_without_tag() { let yaml = "image: nginx"; let manifest = ManifestFile::parse(yaml); assert_eq!(manifest.image_tags.len(), 1); assert_eq!(manifest.image_tags[0], 7..12); } }