use std::{collections::BTreeSet, fmt};
use serde::Deserialize;
#[derive(Debug)]
pub enum ProjectParseError {
InputTooLarge {
actual: usize,
maximum: usize,
},
InvalidYaml(serde_yaml_ng::Error),
EmptyEnvironmentField {
index: usize,
},
}
impl fmt::Display for ProjectParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputTooLarge { actual, maximum } => write!(
formatter,
"ICP project input is {actual} bytes; maximum is {maximum}"
),
Self::InvalidYaml(error) => {
write!(formatter, "invalid effective ICP project YAML: {error}")
}
Self::EmptyEnvironmentField { index } => write!(
formatter,
"ICP environment {index} has an empty name or network"
),
}
}
}
impl std::error::Error for ProjectParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidYaml(error) => Some(error),
_ => None,
}
}
}
#[derive(Deserialize)]
struct EffectiveProject {
#[serde(deserialize_with = "deserialize_environments")]
environments: Vec<Environment>,
}
#[derive(Deserialize)]
struct Environment {
name: String,
network: Network,
}
#[derive(Deserialize)]
struct Network {
name: String,
}
fn deserialize_environments<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<Environment>, D::Error> {
struct EnvironmentVisitor;
impl<'de> serde::de::Visitor<'de> for EnvironmentVisitor {
type Value = Vec<Environment>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an effective ICP environment map")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
use serde::de::Error as _;
let mut names = BTreeSet::new();
let mut environments = Vec::new();
while let Some((key, environment)) = map.next_entry::<String, Environment>()? {
if key != environment.name {
return Err(A::Error::custom("environment key and name disagree"));
}
if !names.insert(key) {
return Err(A::Error::custom("duplicate environment identity"));
}
environments.push(environment);
}
Ok(environments)
}
}
deserializer.deserialize_map(EnvironmentVisitor)
}
pub fn environment_network(
yaml: &[u8],
environment: &str,
maximum_input_bytes: usize,
) -> Result<Option<String>, ProjectParseError> {
if yaml.len() > maximum_input_bytes {
return Err(ProjectParseError::InputTooLarge {
actual: yaml.len(),
maximum: maximum_input_bytes,
});
}
let project: EffectiveProject =
serde_yaml_ng::from_slice(yaml).map_err(ProjectParseError::InvalidYaml)?;
for (index, item) in project.environments.iter().enumerate() {
if item.name.trim().is_empty() || item.network.name.trim().is_empty() {
return Err(ProjectParseError::EmptyEnvironmentField { index });
}
}
Ok(project
.environments
.into_iter()
.find(|item| item.name == environment)
.map(|item| item.network.name))
}
#[cfg(test)]
mod tests;