use crate::document::{DocumentError, read_document};
use molframe_core::contract::{AnalysisPolicy, PolicyParseError};
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum PolicyConfigError {
#[error("policy file I/O failed: {0}")]
Io(#[from] std::io::Error),
#[error("invalid JSON policy: {0}")]
Json(#[from] serde_json::Error),
#[error("invalid TOML policy: {0}")]
Toml(#[from] toml::de::Error),
#[error("policy files must use a .json or .toml suffix")]
UnsupportedFormat,
#[error("invalid policy value for {field}: {value}")]
InvalidValue {
field: &'static str,
value: String,
},
}
impl From<DocumentError> for PolicyConfigError {
fn from(error: DocumentError) -> Self {
match error {
DocumentError::Io(error) => Self::Io(error),
DocumentError::Json(error) => Self::Json(error),
DocumentError::Toml(error) => Self::Toml(error),
DocumentError::UnsupportedFormat => Self::UnsupportedFormat,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ApplicationConfiguration {
pub policy: PolicyOverrides,
pub output: OutputConfiguration,
pub chem: ChemistryConfiguration,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct OutputConfiguration {
pub format: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ChemistryConfiguration {
pub ccd_cache: Option<PathBuf>,
pub ccd_version: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PolicyOverrides {
pub assembly: Option<String>,
pub model: Option<String>,
pub altloc: Option<String>,
pub identifiers: Option<String>,
pub missing_atoms: Option<String>,
pub hydrogens: Option<String>,
pub atom_equivalence: Option<String>,
pub symmetry: Option<String>,
pub alignment: Option<String>,
pub precision: Option<String>,
pub periodic: Option<String>,
pub vdw_radii: Option<String>,
pub contact_def: Option<String>,
pub float_tolerance_relative: Option<f64>,
pub float_tolerance_absolute: Option<f64>,
}
pub fn read_policy(path: impl AsRef<Path>) -> Result<AnalysisPolicy, PolicyConfigError> {
read_configuration(path)?
.policy
.apply_to(AnalysisPolicy::default())
}
pub fn read_configuration(
path: impl AsRef<Path>,
) -> Result<ApplicationConfiguration, PolicyConfigError> {
Ok(read_document(path.as_ref())?)
}
impl PolicyOverrides {
pub fn apply_to(self, mut policy: AnalysisPolicy) -> Result<AnalysisPolicy, PolicyConfigError> {
apply_optional(&mut policy.assembly, self.assembly)?;
apply_optional(&mut policy.model, self.model)?;
apply_optional(&mut policy.altloc, self.altloc)?;
apply_optional(&mut policy.identifiers, self.identifiers)?;
apply_optional(&mut policy.missing_atoms, self.missing_atoms)?;
apply_optional(&mut policy.hydrogens, self.hydrogens)?;
apply_optional(&mut policy.atom_equivalence, self.atom_equivalence)?;
apply_optional(&mut policy.symmetry, self.symmetry)?;
apply_optional(&mut policy.alignment, self.alignment)?;
apply_optional(&mut policy.precision, self.precision)?;
apply_optional(&mut policy.periodic, self.periodic)?;
apply_optional(&mut policy.vdw_radii, self.vdw_radii)?;
apply_optional(&mut policy.contact_def, self.contact_def)?;
if let Some(relative) = self.float_tolerance_relative {
require_nonnegative_finite("float_tolerance_relative", relative)?;
policy.float_tolerance.relative = relative;
}
if let Some(absolute) = self.float_tolerance_absolute {
require_nonnegative_finite("float_tolerance_absolute", absolute)?;
policy.float_tolerance.absolute = absolute;
}
Ok(policy)
}
}
fn apply_optional<T>(target: &mut T, value: Option<String>) -> Result<(), PolicyConfigError>
where
T: std::str::FromStr<Err = PolicyParseError>,
{
if let Some(value) = value {
*target = value.parse()?;
}
Ok(())
}
impl From<PolicyParseError> for PolicyConfigError {
fn from(error: PolicyParseError) -> Self {
Self::InvalidValue {
field: error.field,
value: error.value,
}
}
}
fn require_nonnegative_finite(field: &'static str, value: f64) -> Result<(), PolicyConfigError> {
if value.is_finite() && value >= 0.0 {
Ok(())
} else {
Err(PolicyConfigError::InvalidValue {
field,
value: value.to_string(),
})
}
}
#[cfg(test)]
#[path = "policy_config_tests.rs"]
mod tests;