diff --git a/src/v2/build.rs b/src/v2/build.rs index e0b4eff..2026b81 100644 --- a/src/v2/build.rs +++ b/src/v2/build.rs @@ -15,8 +15,8 @@ pub struct Build { /// Build arguments. #[serde(default, skip_serializing_if = "BTreeMap::is_empty", - deserialize_with = "deserialize_map_or_key_value_list")] - pub args: BTreeMap>, + deserialize_with = "deserialize_map_or_key_optional_value_list")] + pub args: BTreeMap>>, /// PRIVATE. Mark this struct as having unknown fields for future /// compatibility. This prevents direct construction and exhaustive @@ -98,7 +98,7 @@ dockerfile: Dockerfile let build: Build = serde_yaml::from_str(yaml).unwrap(); assert_eq!(build.context, value(Context::new("."))); assert_eq!(build.dockerfile, Some(value("Dockerfile".to_owned()))); - assert_eq!(build.args.get("key").expect("wanted key 'key'").value().unwrap(), + assert_eq!(build.args.get("key").expect("wanted key 'key'").as_ref().unwrap().value().unwrap(), "value"); } @@ -115,16 +115,16 @@ args: let build: Build = serde_yaml::from_str(yaml).unwrap(); // Check type conversion. - assert_eq!(build.args.get("bool").expect("wanted key 'bool'").value().unwrap(), + assert_eq!(build.args.get("bool").expect("wanted key 'bool'").as_ref().unwrap().value().unwrap(), "true"); - assert_eq!(build.args.get("float").expect("wanted key 'float'").value().unwrap(), + assert_eq!(build.args.get("float").expect("wanted key 'float'").as_ref().unwrap().value().unwrap(), "1.5"); - assert_eq!(build.args.get("int").expect("wanted key 'int'").value().unwrap(), + assert_eq!(build.args.get("int").expect("wanted key 'int'").as_ref().unwrap().value().unwrap(), "1"); // Check interpolation. let mut interp: RawOr = - build.args.get("interp").expect("wanted key 'interp'").to_owned(); + build.args.get("interp").expect("wanted key 'interp'").as_ref().unwrap().to_owned(); env::set_var("FOO", "foo"); let env = OsEnvironment::new(); assert_eq!(interp.interpolate_env(&env).unwrap(), "foo") @@ -136,14 +136,10 @@ fn build_args_may_be_a_key_value_list() { context: . args: - key=value + - other_key "; let build: Build = serde_yaml::from_str(yaml).unwrap(); - assert_eq!(build.args.get("key").expect("should have key").value().unwrap(), + assert_eq!(build.args.get("key").expect("should have key").as_ref().unwrap().value().unwrap(), "value"); + assert_eq!(*build.args.get("other_key").expect("should have other_key"), None); } - -// TODO MED: Implement valueless keys. -// -// args: -// - buildno -// - password diff --git a/src/v2/env_file.rs b/src/v2/env_file.rs index c0c062e..44ec10a 100644 --- a/src/v2/env_file.rs +++ b/src/v2/env_file.rs @@ -12,13 +12,13 @@ use super::interpolation::{escape, RawOr}; /// A file pointed to by an `env_file:` field. pub struct EnvFile { /// The variables found in our env file. - vars: BTreeMap, + vars: BTreeMap>, } impl EnvFile { /// Read an `EnvFile` from a stream. pub fn read(input: R) -> Result { - let mut vars: BTreeMap = BTreeMap::new(); + let mut vars: BTreeMap> = BTreeMap::new(); let reader = io::BufReader::new(input); for line_result in reader.lines() { let line = line_result.chain_err(|| "I/O error")?; @@ -28,7 +28,7 @@ impl EnvFile { Regex::new(r#"^\s*(:?#.*)?$"#).unwrap(); // We allow lowercase env vars even if POSIX doesn't. static ref VAR: Regex = - Regex::new(r#"^([_A-Za-z][_A-Za-z0-9]*)=(.*)"#).unwrap(); + Regex::new(r#"^([_A-Za-z][_A-Za-z0-9]*)(=(.*))?"#).unwrap(); } if BLANK.is_match(&line) { @@ -39,7 +39,7 @@ impl EnvFile { .ok_or_else(|| ErrorKind::ParseEnv(line.clone()))?; vars.insert( caps.get(1).unwrap().as_str().to_owned(), - caps.get(2).unwrap().as_str().to_owned(), + caps.get(3).map(|v| v.as_str().to_owned()), ); } Ok(EnvFile { vars: vars }) @@ -54,10 +54,13 @@ impl EnvFile { /// Convert this `EnvFile` to the format we use for the `environment` /// member of `Service`. - pub fn to_environment(&self) -> Result>> { + pub fn to_environment(&self) -> Result>>> { let mut env = BTreeMap::new(); for (k, v) in &self.vars { - env.insert(k.to_owned(), escape(v)?); + env.insert(k.to_owned(), match v.as_ref().map(|v| escape(v)) { + None => None, + Some(v) => Some(v?), + }); } Ok(env) } @@ -79,6 +82,7 @@ fn parses_docker_compatible_env_files() { # These are environment variables: FOO=foo BAR=2 +BAZ # Docker does not currently do anything special with quotes! WEIRD="quoted" @@ -88,7 +92,8 @@ WEIRD="quoted" let cursor = io::Cursor::new(input); let env_file = EnvFile::read(cursor).unwrap(); let env = env_file.to_environment().unwrap(); - assert_eq!(env.get("FOO").unwrap().value().unwrap(), "foo"); - assert_eq!(env.get("BAR").unwrap().value().unwrap(), "2"); - assert_eq!(env.get("WEIRD").unwrap().value().unwrap(), "\"quoted\""); + assert_eq!(env.get("FOO").unwrap().as_ref().unwrap().value().unwrap(), "foo"); + assert_eq!(env.get("BAR").unwrap().as_ref().unwrap().value().unwrap(), "2"); + assert_eq!(*env.get("BAZ").unwrap(), None); + assert_eq!(env.get("WEIRD").unwrap().as_ref().unwrap().value().unwrap(), "\"quoted\""); } diff --git a/src/v2/helpers.rs b/src/v2/helpers.rs index 207a776..a4eeaea 100644 --- a/src/v2/helpers.rs +++ b/src/v2/helpers.rs @@ -95,7 +95,7 @@ impl<'de> Deserialize<'de> for ConvertToString { /// /// ```text /// struct Example { -/// #[serde(deserialize_with = "deserialize_hash_or_key_value_list")] +/// #[serde(deserialize_with = "deserialize_map_or_key_value_list")] /// pub args: BTreeMap>, /// } /// ``` @@ -165,7 +165,83 @@ where } } - deserializer.deserialize_map(MapOrKeyValueListVisitor) + deserializer.deserialize_any(MapOrKeyValueListVisitor) +} + +/// Like `deserialize_map_or_key_value_list`, but allowing missing values +/// (e.g. for environment variables) +pub fn deserialize_map_or_key_optional_value_list<'de, D>( + deserializer: D, +) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + /// Declare an internal visitor type to handle our input. + struct MapOrKeyOptionalValueListVisitor; + + impl<'de> Visitor<'de> for MapOrKeyOptionalValueListVisitor { + type Value = BTreeMap>>; + + // We have a real map. + fn visit_map(self, mut visitor: V) -> Result + where + V: MapAccess<'de>, + { + let mut map: BTreeMap>> = BTreeMap::new(); + while let Some(key) = visitor.next_key::()? { + if map.contains_key(&key) { + let msg = format!("duplicate map key: {}", &key); + return Err(::custom(msg)); + } + let ConvertToString(val) = visitor.next_value::()?; + let raw_or_value = raw(val) + .map_err(|e| ::custom(format!("{}", e)))?; + map.insert(key, Some(raw_or_value)); + } + Ok(map) + } + + // We have a key/value list. Yuck. + fn visit_seq(self, mut visitor: V) -> Result + where + V: SeqAccess<'de>, + { + lazy_static! { + // Match a key/value pair. + static ref KEY_VALUE: Regex = + Regex::new("^([^=]+)(=(.*))?$").unwrap(); + } + + let mut map: BTreeMap>> = BTreeMap::new(); + while let Some(key_value) = visitor.next_element::()? { + let caps = KEY_VALUE.captures(&key_value).ok_or_else(|| { + let msg = format!("expected KEY[=value], got: <{}>", &key_value); + ::custom(msg) + })?; + let key = caps.get(1).unwrap().as_str(); + let value = caps.get(3).map(|v| v.as_str()); + if map.contains_key(key) { + let msg = format!("duplicate map key: {}", key); + return Err(::custom(msg)); + } + let optional_raw_or_value = match value.map(|value| { + raw(value.to_owned()) + .map_err(|e| ::custom(format!("{}", e))) + }) { + None => None, + Some(x) => Some(x?), + }; + map.insert(key.to_owned(), optional_raw_or_value); + } + Ok(map) + } + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a map or a key/[value] list") + } + } + + deserializer.deserialize_any(MapOrKeyOptionalValueListVisitor) } /// Given a map, deserialize it normally. But if we have a list of string @@ -211,7 +287,7 @@ where } } - deserializer.deserialize_map(MapOrDefaultListVisitor(PhantomData::)) + deserializer.deserialize_any(MapOrDefaultListVisitor(PhantomData::)) } /// Deserialize either list or a single bare string as a list. diff --git a/src/v2/network.rs b/src/v2/network.rs index 61197b2..e54a044 100644 --- a/src/v2/network.rs +++ b/src/v2/network.rs @@ -39,8 +39,8 @@ pub struct Network { /// Docker labels for this volume, specifying various sorts of /// custom metadata. #[serde(default, skip_serializing_if = "BTreeMap::is_empty", - deserialize_with = "deserialize_map_or_key_value_list")] - pub labels: BTreeMap>, + deserialize_with = "deserialize_map_or_key_optional_value_list")] + pub labels: BTreeMap>>, // TODO LOW: ipam diff --git a/src/v2/service.rs b/src/v2/service.rs index dd386d4..3ee1c71 100644 --- a/src/v2/service.rs +++ b/src/v2/service.rs @@ -72,8 +72,8 @@ pub struct Service { /// Environment variables and values to supply to the container. #[serde(default, skip_serializing_if = "BTreeMap::is_empty", - deserialize_with = "deserialize_map_or_key_value_list")] - pub environment: BTreeMap>, + deserialize_with = "deserialize_map_or_key_optional_value_list")] + pub environment: BTreeMap>>, /// Expose a list of ports to any containers that link to us. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -98,8 +98,8 @@ pub struct Service { /// Docker labels for this container, specifying various sorts of /// custom metadata. #[serde(default, skip_serializing_if = "BTreeMap::is_empty", - deserialize_with = "deserialize_map_or_key_value_list")] - pub labels: BTreeMap>, + deserialize_with = "deserialize_map_or_key_optional_value_list")] + pub labels: BTreeMap>>, /// Links to other services in this file. #[serde(default, skip_serializing_if = "Vec::is_empty")] diff --git a/src/v2/volume.rs b/src/v2/volume.rs index dfd20bb..09181ab 100644 --- a/src/v2/volume.rs +++ b/src/v2/volume.rs @@ -31,8 +31,8 @@ pub struct Volume { /// Docker labels for this volume, specifying various sorts of /// custom metadata. #[serde(default, skip_serializing_if = "BTreeMap::is_empty", - deserialize_with = "deserialize_map_or_key_value_list")] - pub labels: BTreeMap>, + deserialize_with = "deserialize_map_or_key_optional_value_list")] + pub labels: BTreeMap>>, /// PRIVATE. Mark this struct as having unknown fields for future /// compatibility. This prevents direct construction and exhaustive