terragrate 0.4.0

Simplifies migrating terraform state based on migration files
Documentation
use crate::command::Command;
use crate::element::Element;
use serde_derive::Deserialize;

#[derive(Deserialize, PartialEq, Debug)]
pub enum TransformationType {
    MV,
    RM,
}

pub struct TransformationResult {
    pub element: Element,
    pub command: Command,
}

#[derive(Deserialize, PartialEq, Debug)]
pub struct Transformation {
    pub kind: TransformationType,
    pub matcher: String,
    pub replacement: String,
}

impl Transformation {
    pub fn apply(&self, e: &Element) -> TransformationResult {
        match self.kind {
            TransformationType::MV => self.mv(e),
            TransformationType::RM => self.rm(e),
        }
    }

    fn mv(&self, e: &Element) -> TransformationResult {
        let new_element = match e {
            Element::Resource(r) => Element::Resource(r.replace(&self.matcher, &self.replacement)),
            Element::Empty => Element::Empty,
        };

        let command = match new_element {
            Element::Resource(_) if *e == new_element => Command::NoOp,
            Element::Resource(_) => Command::MV {
                source: e.to_string(),
                target: new_element.to_string(),
            },
            Element::Empty => Command::NoOp,
        };

        TransformationResult {
            element: new_element,
            command,
        }
    }

    fn rm(&self, e: &Element) -> TransformationResult {
        let (new_element, command) = match e {
            Element::Resource(r) if r.contains(&self.matcher) => {
                (Element::Empty, Command::RM(r.to_string()))
            }
            Element::Resource(_) => (e.clone(), Command::NoOp),
            Element::Empty => (Element::Empty, Command::NoOp),
        };

        TransformationResult {
            element: new_element,
            command,
        }
    }
}

#[cfg(test)]
mod transform_tests {
    use super::*;

    fn element() -> Element {
        Element::Resource("module.public_network.docker_network.network".to_string())
    }

    fn mv() -> Transformation {
        Transformation {
            kind: TransformationType::MV,
            matcher: "public_network".to_string(),
            replacement: "private_network".to_string(),
        }
    }

    fn rm(matcher: &str) -> Transformation {
        Transformation {
            kind: TransformationType::RM,
            matcher: matcher.to_string(),
            replacement: "".to_string(),
        }
    }

    #[test]
    fn test_mv_apply_leaves_element_unchanged_if_it_doesnt_apply() {
        let t = Transformation {
            kind: TransformationType::MV,
            matcher: "private_network".to_string(),
            replacement: "error".to_string(),
        };
        assert_eq!(element(), t.apply(&element()).element)
    }

    #[test]
    fn test_mv_apply_empty_command_if_it_doesnt_apply() {
        let t = Transformation {
            kind: TransformationType::MV,
            matcher: "private_network".to_string(),
            replacement: "error".to_string(),
        };
        assert_eq!(Command::NoOp, t.apply(&element()).command)
    }

    #[test]
    fn test_mv_apply_changes_element() {
        assert_eq!(
            Element::Resource("module.private_network.docker_network.network".to_string()),
            mv().apply(&element()).element
        )
    }

    #[test]
    fn test_mv_apply_creates_mv_command() {
        assert_eq!(
            Command::MV {
                source: element().to_string(),
                target: "module.private_network.docker_network.network".to_string()
            },
            mv().apply(&element()).command
        )
    }

    #[test]
    fn test_rm_apply_leaves_element_unchanged_if_it_doesnt_apply() {
        assert_eq!(element(), rm("private_network").apply(&element()).element)
    }

    #[test]
    fn test_rm_apply_empty_command_if_it_doesnt_apply() {
        assert_eq!(
            Command::NoOp,
            rm("private_network").apply(&element()).command
        )
    }

    #[test]
    fn test_rm_apply_removes_element() {
        assert_eq!(
            Element::Empty,
            rm("public_network").apply(&element()).element
        )
    }

    #[test]
    fn test_rm_apply_creates_rm_command() {
        assert_eq!(
            Command::RM("module.public_network.docker_network.network".to_string()),
            rm("public_network").apply(&element()).command
        )
    }
}