buildkit-frontend 0.1.0

Foundation for BuildKit frontends implemented in Rust
Documentation
use std::collections::BTreeMap;
use std::env;
use std::io::Cursor;

#[derive(Debug, PartialEq)]
pub struct Options {
    inner: BTreeMap<String, OptionValue>,
}

#[derive(Debug, PartialEq)]
enum OptionValue {
    Flag(bool),
    Arguments(Vec<String>),
}

impl Options {
    pub fn analyse() -> Self {
        let env_iter = env::vars().filter_map(|(key, value)| {
            if key.starts_with("BUILDKIT_FRONTEND_OPT_") {
                Some(value)
            } else {
                None
            }
        });

        Self::from(env_iter)
    }

    pub fn has<S>(&self, name: S) -> bool
    where
        S: AsRef<str>,
    {
        match self.inner.get(name.as_ref()) {
            Some(container) => match container {
                OptionValue::Flag(exists) => *exists,
                OptionValue::Arguments(_) => true,
            },

            None => false,
        }
    }

    pub fn is_flag_set<S>(&self, name: S) -> bool
    where
        S: AsRef<str>,
    {
        match self.inner.get(name.as_ref()) {
            Some(container) => match container {
                OptionValue::Flag(flag) => *flag,
                OptionValue::Arguments(_) => false,
            },

            None => false,
        }
    }

    pub fn has_value<S1, S2>(&self, name: S1, value: S2) -> bool
    where
        S1: AsRef<str>,
        S2: AsRef<str>,
    {
        match self.inner.get(name.as_ref()) {
            Some(container) => match container {
                OptionValue::Flag(_) => false,
                OptionValue::Arguments(values) => values.iter().any(|item| item == value.as_ref()),
            },

            None => false,
        }
    }

    pub fn get<S>(&self, name: S) -> Option<&str>
    where
        S: AsRef<str>,
    {
        match self.inner.get(name.as_ref()) {
            Some(container) => match container {
                OptionValue::Flag(_) => None,
                OptionValue::Arguments(values) => values.iter().map(String::as_str).next(),
            },

            None => None,
        }
    }

    fn extract_name_and_value(mut raw_value: String) -> (String, OptionValue) {
        if raw_value.starts_with("build-arg:") {
            raw_value = raw_value.trim_start_matches("build-arg:").into();
        }

        let delimiter_pos = raw_value.find('=');

        match delimiter_pos {
            None => (raw_value, OptionValue::Flag(true)),

            Some(pos) if &raw_value[pos + 1..] == "false" => {
                (raw_value[..pos].into(), OptionValue::Flag(false))
            }

            Some(pos) if &raw_value[pos + 1..] == "true" => {
                (raw_value[0..pos].into(), OptionValue::Flag(true))
            }

            Some(pos) => {
                let mut builder = csv::ReaderBuilder::new();
                builder.has_headers(false);

                let values = {
                    builder
                        .from_reader(Cursor::new(&raw_value[pos + 1..]))
                        .deserialize::<Vec<String>>()
                        .next()
                        .and_then(|result| result.ok())
                };

                match values {
                    Some(values) => (raw_value[..pos].into(), OptionValue::Arguments(values)),
                    None => (raw_value, OptionValue::Flag(true)),
                }
            }
        }
    }
}

impl<T> From<T> for Options
where
    T: Iterator<Item = String>,
{
    fn from(iter: T) -> Self {
        Self {
            inner: iter.map(Self::extract_name_and_value).collect(),
        }
    }
}

#[test]
fn options_parsing() {
    assert_eq!(
        Options::extract_name_and_value("name".into()),
        ("name".into(), OptionValue::Flag(true))
    );

    assert_eq!(
        Options::extract_name_and_value("name=true".into()),
        ("name".into(), OptionValue::Flag(true))
    );

    assert_eq!(
        Options::extract_name_and_value("name=false".into()),
        ("name".into(), OptionValue::Flag(false))
    );

    assert_eq!(
        Options::extract_name_and_value("name=".into()),
        ("name=".into(), OptionValue::Flag(true))
    );

    assert_eq!(
        Options::extract_name_and_value("name=value".into()),
        ("name".into(), OptionValue::Arguments(vec!["value".into()]))
    );

    assert_eq!(
        Options::extract_name_and_value("name=de=limiter".into()),
        (
            "name".into(),
            OptionValue::Arguments(vec!["de=limiter".into()])
        )
    );

    assert_eq!(
        Options::extract_name_and_value("name=false,true".into()),
        (
            "name".into(),
            OptionValue::Arguments(vec!["false".into(), "true".into()])
        )
    );

    assert_eq!(
        Options::extract_name_and_value("name=value1,value2,value3".into()),
        (
            "name".into(),
            OptionValue::Arguments(vec!["value1".into(), "value2".into(), "value3".into()])
        )
    );

    assert_eq!(
        Options::extract_name_and_value("name=value1,val=ue2,value3".into()),
        (
            "name".into(),
            OptionValue::Arguments(vec!["value1".into(), "val=ue2".into(), "value3".into()])
        )
    );

    assert_eq!(
        Options::extract_name_and_value("name=\"value1,value2\",value3".into()),
        (
            "name".into(),
            OptionValue::Arguments(vec!["value1,value2".into(), "value3".into()])
        )
    );

    assert_eq!(
        Options::extract_name_and_value("build-arg:name".into()),
        ("name".into(), OptionValue::Flag(true))
    );

    assert_eq!(
        Options::extract_name_and_value("build-arg:name=value".into()),
        ("name".into(), OptionValue::Arguments(vec!["value".into()]))
    );
}

#[test]
fn has_method() {
    let options = Options::from(
        vec![
            "option1",
            "option2=true",
            "option3=false",
            "option4=true,false",
        ]
        .into_iter()
        .map(String::from),
    );

    assert_eq!(options.has("option1"), true);
    assert_eq!(options.has("option2"), true);
    assert_eq!(options.has("option3"), false);
    assert_eq!(options.has("option4"), true);
}

#[test]
fn has_value_method() {
    let options = Options::from(
        vec!["option1", "option2=true", "option3=true,false,any_other"]
            .into_iter()
            .map(String::from),
    );

    assert_eq!(options.has_value("option1", ""), false);
    assert_eq!(options.has_value("option1", "any_other"), false);
    assert_eq!(options.has_value("option2", ""), false);
    assert_eq!(options.has_value("option2", "any_other"), false);
    assert_eq!(options.has_value("option3", "true"), true);
    assert_eq!(options.has_value("option3", "false"), true);
    assert_eq!(options.has_value("option3", "any_other"), true);
    assert_eq!(options.has_value("option3", "missing"), false);
}