nixify 0.2.0

A simple tool to parse existing configuration files into nix syntax
Documentation
use std::path::PathBuf;
pub mod json;
pub mod toml;
pub mod yaml;

use clap::ValueEnum;
use indexmap::IndexMap;

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, ValueEnum)]
#[allow(non_camel_case_types)]
pub enum SupportedFormats {
    yaml,
    toml,
    json,
}

pub trait Parser {
    fn parse(&self, content: &str) -> Option<Vec<NixVariable>>;
}

#[derive(Debug, Clone, PartialEq)]
pub struct NixVariable {
    pub name: String,
    pub value: NixVariableValue,
}

impl NixVariable {
    pub fn to_string(&self) -> String {
        format!("{} = {};\n", self.name, self.value.to_string())
    }
    pub fn new(name: &str, value: &NixVariableValue) -> NixVariable {
        NixVariable {
            name: name.to_owned(),
            value: value.to_owned(),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum NixVariableValue {
    String(String),
    Number(f64),
    Path(Box<PathBuf>),
    Boolean(bool),
    Null,
    List(Vec<NixVariableValue>),
    AttributeSet(IndexMap<String, NixVariableValue>),
}

impl NixVariableValue {
    pub fn to_string(&self) -> String {
        match self {
            Self::Number(n) => format!("{}", n),
            Self::String(s) => format!("\"{}\"", s),
            Self::Path(p) => format!("{}", p.to_str().expect("Error parsing file.")),
            Self::Boolean(b) => format!("{}", b),
            Self::Null => "null".to_string(),
            Self::AttributeSet(a) => format!(
                "{{\n{}}}",
                a.into_iter()
                    .map(|(key, value)| NixVariable::new(key, value).to_string())
                    .reduce(|acc, e| acc + &e)
                    .expect("Error parsing file")
            ),
            Self::List(l) => format!(
                "[\n{}\n]",
                l.into_iter()
                    .map(|f| f.to_string())
                    .reduce(|acc, e| acc + "\n" + &e)
                    .unwrap()
            ),
        }
    }
}

pub struct ExpressionParser {
    parsers: IndexMap<SupportedFormats, Box<dyn 'static + Parser>>,
    guess_format: bool,
}

impl ExpressionParser {
    pub fn new() -> ExpressionParser {
        ExpressionParser {
            parsers: IndexMap::new(),
            guess_format: false,
        }
    }

    pub fn add_parser(
        mut self,
        format: SupportedFormats,
        parser: Box<dyn Parser>,
    ) -> Option<ExpressionParser> {
        if self.parsers.contains_key(&format) {
            None
        } else {
            self.parsers.insert(format, parser);
            Some(self)
        }
    }

    pub fn with_format_guessing(mut self) -> ExpressionParser {
        self.guess_format = true;
        self
    }

    pub fn parse(
        &self,
        content: &str,
        format: &Option<SupportedFormats>,
    ) -> Option<Vec<NixVariable>> {
        if format.is_none() && self.guess_format {
            self.parsers
                .iter()
                .map(|(_format, parser)| parser.parse(content))
                .filter(|parsed| parsed.is_some())
                .last()
                .flatten()
        } else if format.is_some() && self.parsers.contains_key(&format.unwrap()) {
            self.parsers[&format.unwrap()].parse(content)
        } else {
            None
        }
    }
}

pub struct ExpressionGenerator {
    formatting: bool,
}

impl ExpressionGenerator {
    pub fn new() -> ExpressionGenerator {
        ExpressionGenerator { formatting: false }
    }
    pub fn with_formatting(mut self) -> ExpressionGenerator {
        self.formatting = true;
        self
    }
    pub fn generate_nix_expression(&self, name: &str, values: &Vec<NixVariable>) -> Option<String> {
        vec![
            "{ config, pkgs, ... }:\n".to_string(),
            "{\n".to_string(),
            format!("programs.{}.enable = true;\n", name),
        ]
        .into_iter()
        .chain(values.into_iter().map(|v| v.to_string()))
        .chain(vec!["};\n".to_string(), "}".to_string()])
        .reduce(|acc, e| format!("{acc}{e}"))
        .map(|expression| {
            if self.formatting {
                nixpkgs_fmt::reformat_string(&expression)
            } else {
                expression
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::{
        json::JsonParser, toml::TomlParser, yaml::YamlParser, ExpressionGenerator,
        ExpressionParser, NixVariable, NixVariableValue,
    };
    use indexmap::IndexMap;
    use std::path::Path;

    #[test]
    fn test_expression_generator() {
        let expression_generator = ExpressionGenerator::new();
        let expression = vec![
            NixVariable::new(
                "foo",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "bar".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([
                        ("a".to_string(), NixVariableValue::Number(1.0)),
                        (
                            "b".to_string(),
                            NixVariableValue::String("test".to_string()),
                        ),
                    ])),
                )])),
            ),
            NixVariable::new(
                "this",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "is".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([(
                        "a".to_string(),
                        NixVariableValue::AttributeSet(IndexMap::from([(
                            "float".to_string(),
                            NixVariableValue::Number(0.1),
                        )])),
                    )])),
                )])),
            ),
        ];
        let expected = "{ config, pkgs, ... }:\n{\nprograms.test.enable = true;\nfoo = {\nbar = {\na = 1;\nb = \"test\";\n};\n};\nthis = {\nis = {\na = {\nfloat = 0.1;\n};\n};\n};\n};\n}";
        let generated = expression_generator.generate_nix_expression("test", &expression);
        assert!(generated.is_some());
        assert_eq!(generated.unwrap(), expected);
    }

    #[test]
    fn test_expression_generator_formatted() {
        let expression_generator = ExpressionGenerator::new().with_formatting();
        let expression = vec![
            NixVariable::new(
                "foo",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "bar".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([
                        ("a".to_string(), NixVariableValue::Number(1.0)),
                        (
                            "b".to_string(),
                            NixVariableValue::String("test".to_string()),
                        ),
                    ])),
                )])),
            ),
            NixVariable::new(
                "this",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "is".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([(
                        "a".to_string(),
                        NixVariableValue::AttributeSet(IndexMap::from([(
                            "float".to_string(),
                            NixVariableValue::Number(0.1),
                        )])),
                    )])),
                )])),
            ),
        ];
        let expected = "{ config, pkgs, ... }:\n{\n  programs.test.enable = true;\n  foo = {\n    bar = {\n      a = 1;\n      b = \"test\";\n    };\n  };\n  this = {\n    is = {\n      a = {\n        float = 0.1;\n      };\n    };\n  };\n};\n}\n";
        let generated = expression_generator.generate_nix_expression("test", &expression);
        assert!(generated.is_some());
        assert_eq!(generated.unwrap(), expected);
    }

    #[test]
    fn test_variable_conversion() {
        let number = NixVariable::new("number", &NixVariableValue::Number(4.2));
        let string = NixVariable::new("string", &NixVariableValue::String("foobar".to_string()));
        let path = NixVariable::new(
            "path",
            &NixVariableValue::Path(Box::new(Path::new("/tmp/foo").to_path_buf())),
        );
        let bool = NixVariable::new("bool", &NixVariableValue::Boolean(true));
        let null = NixVariable::new("null", &NixVariableValue::Null);
        let list = NixVariable::new(
            "list",
            &NixVariableValue::List(vec![
                NixVariableValue::Number(4.2),
                NixVariableValue::Number(6.9),
            ]),
        );
        let attrset = NixVariable::new(
            "attrset",
            &NixVariableValue::AttributeSet(IndexMap::from([(
                "foo".to_string(),
                NixVariableValue::String("bar".to_string()),
            )])),
        );

        assert_eq!(number.to_string(), "number = 4.2;\n");
        assert_eq!(string.to_string(), "string = \"foobar\";\n");
        assert_eq!(path.to_string(), "path = /tmp/foo;\n");
        assert_eq!(bool.to_string(), "bool = true;\n");
        assert_eq!(null.to_string(), "null = null;\n");
        assert_eq!(list.to_string(), "list = [\n4.2\n6.9\n];\n");
        assert_eq!(attrset.to_string(), "attrset = {\nfoo = \"bar\";\n};\n");
    }
    #[test]
    fn test_format_guessing() {
        let parser = ExpressionParser::new()
            .add_parser(super::SupportedFormats::yaml, Box::new(YamlParser::new()))
            .unwrap()
            .add_parser(super::SupportedFormats::toml, Box::new(TomlParser::new()))
            .unwrap()
            .add_parser(super::SupportedFormats::json, Box::new(JsonParser::new()))
            .unwrap()
            .with_format_guessing();
        let yaml = "
foo:
    bar:
        a: 1
        b: 'test'
this:
    is:
        a:
            float: 0.1
# Comment
            ";
        let toml = "
[foo.bar]
a = 1
b = \"test\"
[this.is.a]
float = 0.1
# A comment
            ";
        let json = "
{
    \"foo\": {
        \"bar\": {
            \"a\": 1,
            \"b\": \"test\"
        }
    },
    \"this\": {
        \"is\": {
            \"a\" : {
                \"float\": 0.1
            }
        }
    }
}
";
        let expected = vec![
            NixVariable::new(
                "foo",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "bar".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([
                        ("a".to_string(), NixVariableValue::Number(1.0)),
                        (
                            "b".to_string(),
                            NixVariableValue::String("test".to_string()),
                        ),
                    ])),
                )])),
            ),
            NixVariable::new(
                "this",
                &NixVariableValue::AttributeSet(IndexMap::from([(
                    "is".to_string(),
                    NixVariableValue::AttributeSet(IndexMap::from([(
                        "a".to_string(),
                        NixVariableValue::AttributeSet(IndexMap::from([(
                            "float".to_string(),
                            NixVariableValue::Number(0.1),
                        )])),
                    )])),
                )])),
            ),
        ];
        let yaml_result = parser.parse(&yaml, &None);
        let toml_result = parser.parse(&toml, &None);
        let json_result = parser.parse(&json, &None);
        assert!(yaml_result.is_some());
        assert_eq!(yaml_result.unwrap(), expected);
        assert!(toml_result.is_some());
        assert_eq!(toml_result.unwrap(), expected);
        assert!(json_result.is_some());
        assert_eq!(json_result.unwrap(), expected);
    }
}