use super::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct File {
pub version: String,
pub services: BTreeMap<String, Service>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "deserialize_map_struct_or_null"
)]
pub volumes: BTreeMap<String, Volume>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "deserialize_map_struct_or_null"
)]
pub networks: BTreeMap<String, Network>,
#[doc(hidden)]
#[serde(default, skip_serializing, skip_deserializing)]
pub _hidden: (),
}
derive_standard_impls_for!(File, {
version, services, volumes, networks, _hidden
});
impl File {
pub fn read<R>(r: R) -> Result<Self>
where
R: io::Read,
{
let file = serde_yaml::from_reader(r)?;
validate_file(&file)?;
Ok(file)
}
pub fn write<W>(&self, w: &mut W) -> Result<()>
where
W: io::Write,
{
validate_file(self)?;
Ok(serde_yaml::to_writer(w, self)?)
}
pub fn read_from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let f = fs::File::open(path)
.map_err(|err| Error::read_file(path.to_owned(), err))?;
Self::read(io::BufReader::new(f))
.map_err(|err| Error::read_file(path.to_owned(), err))
}
pub fn write_to_path<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let f = fs::File::create(path)
.map_err(|err| Error::write_file(path.to_owned(), err))?;
self.write(&mut io::BufWriter::new(f))
.map_err(|err| Error::write_file(path.to_owned(), err))
}
pub fn inline_all(&mut self, base: &Path) -> Result<()> {
for service in self.services.values_mut() {
service.inline_all(base)?;
}
Ok(())
}
pub fn make_standalone(&mut self, base: &Path) -> Result<()> {
self.interpolate_all()?;
self.inline_all(base)
}
}
impl Default for File {
fn default() -> File {
File {
version: "2.4".to_owned(),
services: Default::default(),
volumes: Default::default(),
networks: Default::default(),
_hidden: (),
}
}
}
impl FromStr for File {
type Err = Error;
fn from_str(s: &str) -> Result<File> {
Self::read(io::Cursor::new(s))
}
}
#[test]
#[cfg_attr(feature = "clippy", allow(blacklisted_name))]
fn file_can_be_converted_from_and_to_yaml_version_2() {
let yaml = r#"---
services:
foo:
build: .
version: "2"
volumes:
db:
external: true
"#;
assert_roundtrip!(File, yaml);
let file = File::from_str(&yaml).unwrap();
let foo = file.services.get("foo").unwrap();
assert_eq!(
foo.build.as_ref().unwrap().context,
value(Context::new("."))
);
}
#[test]
fn file_can_be_converted_from_and_to_yaml_version_2_4() {
let yaml = r#"---
services:
foo:
build: .
version: "2.4"
volumes:
db:
external: true
"#;
assert_roundtrip!(File, yaml);
}
#[test]
fn file_allows_null_volumes_and_networks() {
let yaml = r#"---
"services":
"foo":
"build": "."
"networks":
"frontend":
"internal":
"version": "2"
"volumes":
"bar":
"foo":
"#;
let file = File::from_str(&yaml).unwrap();
assert_eq!(file.volumes.len(), 2);
assert_eq!(file.networks.len(), 2);
}
#[test]
fn file_checks_version_number() {
let yaml = r#"---
"services":
"foo":
"build": "."
"version": "100"
"#;
assert!(File::from_str(&yaml).is_err());
}
#[test]
#[ignore]
fn file_validates_against_schema() {
let yaml = r#"---
"version": "2"
"services":
# An invalid service name:
"foo!":
"build": "."
"#;
assert!(File::from_str(&yaml).is_err());
}