Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ mod options;
pub mod secret;
mod serde;
pub mod service;
mod variable_interpolation;
mod volume;

use std::{
Expand Down
66 changes: 62 additions & 4 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
//! [`Options`] builder for deserialization options for a [`Compose`] file.

use std::io::Read;
use std::{collections::HashMap, io::Read};

use crate::{Compose, YamlValue};
use crate::{
variable_interpolation::{yaml_walk::interpolate_value, VariableResolver},
Compose, YamlValue,
};

/// Deserialization options builder for a [`Compose`] file.
#[allow(missing_copy_implementations)] // Will include interpolation vars as a HashMap.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Options {
/// Whether to perform merging of `<<` keys.
apply_merge: bool,
/// the variables to perform environment variable interpolation on, if any
interpolate_vars: Option<VariableResolver>,
}

impl Options {
Expand Down Expand Up @@ -49,10 +54,58 @@ impl Options {
self
}

/// Add a map of values to interpolate variable placeholders in YAML before constructing the `Compose`
/// instance.
/// The given `HashMap` could be sourced from dotenv files or the processes' environment, or both.
/// Can be called multiple times to merge different variable sources.
/// The last mapping for any given variable name wins.
///
/// ```
/// use compose_spec::Compose;
/// use std::collections::HashMap;
///
/// let yaml = "\
/// services:
/// one:
/// environment:
/// FOO: $FOO # simple variable without default
/// BAR: ${BAR-Bar-Default} # braced variable with default value
/// BAZ: $$ESCAPED # double-$ to write a literal $
/// ";
///
/// let vars = HashMap::from([
/// ("FOO".into(), "Foo-Value".into())
/// ]);
///
/// let compose = Compose::options()
/// .interpolate_vars(vars)
/// .from_yaml_str(yaml)
/// .unwrap();
///
/// let one_env = compose.services["one"]
/// .environment
/// .clone()
/// .into_map()
/// .unwrap();
///
/// assert_eq!(one_env["FOO"].as_ref().unwrap().as_string().unwrap(), "Foo-Value");
/// assert_eq!(one_env["BAR"].as_ref().unwrap().as_string().unwrap(), "Bar-Default");
/// assert_eq!(one_env["BAZ"].as_ref().unwrap().as_string().unwrap(), "$ESCAPED");
/// ```
pub fn interpolate_vars(&mut self, vars: HashMap<String, String>) -> &mut Self {
let mut resolver = self.interpolate_vars.take().unwrap_or_default();
resolver.add_vars(vars);
let _ = self.interpolate_vars.replace(resolver);
self
}

/// Return `true` if any options are set.
const fn any(&self) -> bool {
let Self { apply_merge } = *self;
apply_merge
let Self {
apply_merge,
interpolate_vars,
} = self;
*apply_merge || interpolate_vars.is_some()
}

/// Use the set options to deserialize a [`Compose`] file from a string slice of YAML.
Expand Down Expand Up @@ -103,6 +156,11 @@ impl Options {
if self.apply_merge {
value.apply_merge()?;
}

if let Some(vars) = &self.interpolate_vars {
interpolate_value(vars, &mut value)?;
}

serde_yaml::from_value(value)
}
}
32 changes: 32 additions & 0 deletions src/variable_interpolation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Implementation of the environment variable interpolation as defined in the
//! [compose spec](https://github.com/compose-spec/compose-spec/blob/main/12-interpolation.md)
//!
//! `YamlValues` are interpolated in-place.

use std::collections::HashMap;

mod error;
mod parser;
pub(crate) mod yaml_walk;

#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// Resolves values for a given key from the relevant sources (.env files, process environment, ...)
pub(crate) struct VariableResolver {
/// map of all resolvable variable names
vars: HashMap<String, String>,
}

impl VariableResolver {
/// add some name <-> value mappings to the interpolation. last definition of a name wins.
pub(crate) fn add_vars(&mut self, new_vars: HashMap<String, String>) -> &mut Self {
for (key, value) in new_vars {
let _ = self.vars.insert(key, value);
}
self
}

/// get the value associated with the given key.
pub(crate) fn get(&self, key: impl AsRef<str>) -> Option<&str> {
self.vars.get(key.as_ref()).map(String::as_str)
}
}
66 changes: 66 additions & 0 deletions src/variable_interpolation/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! contains the error type used by the variable interpolation parser
//!
//! since the calling code expects the error type to be `serde_yaml::Error`,
//! `ParseError` can be converted to that type, using the error information
//! to construct a custom error message

use serde::de::Error;

/// the error type used for variable interpolation
#[derive(Debug)]
pub(super) struct ParseError {
/// error message displayed to the user
message: String,
/// the current output string at the time of failure
output: String,
/// the rest of the input at the time of failure
rest: String,
}

impl ParseError {
/// construct a new error.
/// `message` should be a human-readable error message.
/// `output` is the current state of the parser's output buffer.
/// `rest` contains the rest of the parser's unparsed input.
pub(super) const fn new(message: String, output: String, rest: String) -> Self {
Self {
message,
output,
rest,
}
}
}

impl From<ParseError> for serde_yaml::Error {
fn from(value: ParseError) -> Self {
let ParseError {
message,
output,
rest,
} = value;
let msg = format!("failed ({message}) around {output} ... {rest}");
Self::custom(msg)
}
}

/// convenience macro to quickly construct and return a parsing error
/// from a message and the current parser state.
/// the first argument must be the parser, from the second argument onwards
/// it works like format!()
macro_rules! terminate {
($slf:ident, $msg:literal $(,)? $($fragments:expr),*) => {{
let slf: &crate::variable_interpolation::parser::Parser = $slf;
let message: ::std::string::String = format!($msg, $($fragments),* );
let output: ::std::string::String = slf.output.to_owned();
let rest: ::std::string::String = slf.rest.clone().collect();
return ::std::result::Result::Err(
crate::variable_interpolation::error::ParseError::new(
message,
output,
rest,
),
);
}};
}

pub(super) use terminate;
Loading