aep-core 0.1.1

Transport-independent protocol primitives for the Agent Enrollment Protocol.
Documentation
use std::{collections::HashSet, hash::Hash};

use serde::de::DeserializeOwned;

use crate::{ParseError, ValidationError, ValidationIssue};

pub(crate) fn parse_and_validate<T>(
    data: &[u8],
    document_type: &str,
    validate: impl FnOnce(&T) -> Result<(), ValidationError>,
) -> Result<T, ParseError>
where
    T: DeserializeOwned,
{
    let mut deserializer = serde_json::Deserializer::from_slice(data);
    let value = match serde_path_to_error::deserialize::<_, T>(&mut deserializer) {
        Ok(value) => value,
        Err(error) if error.inner().is_syntax() || error.inner().is_eof() => {
            return Err(ParseError::Json {
                document_type: document_type.to_owned(),
                source: error.into_inner(),
            });
        }
        Err(error) => {
            return Err(ValidationError {
                document_type: document_type.to_owned(),
                issues: vec![ValidationIssue {
                    path: json_path(&error.path().to_string()),
                    message: error.inner().to_string(),
                }],
            }
            .into());
        }
    };
    deserializer.end().map_err(|source| ParseError::Json {
        document_type: document_type.to_owned(),
        source,
    })?;
    validate(&value)?;
    Ok(value)
}

pub(crate) fn deserialize_optional_non_null<'de, D, T>(
    deserializer: D,
) -> Result<Option<T>, D::Error>
where
    D: serde::Deserializer<'de>,
    T: serde::Deserialize<'de>,
{
    T::deserialize(deserializer).map(Some)
}

pub(crate) fn result(
    document_type: &str,
    issues: Vec<ValidationIssue>,
) -> Result<(), ValidationError> {
    if issues.is_empty() {
        Ok(())
    } else {
        Err(ValidationError {
            document_type: document_type.to_owned(),
            issues,
        })
    }
}

pub(crate) fn issue(path: impl Into<String>, message: impl Into<String>) -> ValidationIssue {
    ValidationIssue {
        path: path.into(),
        message: message.into(),
    }
}

pub(crate) fn require_non_empty(value: &str, path: &str, issues: &mut Vec<ValidationIssue>) {
    if value.is_empty() {
        issues.push(issue(path, "Expected a non-empty string."));
    }
}

pub(crate) fn validate_optional_non_empty(
    value: Option<&str>,
    path: &str,
    issues: &mut Vec<ValidationIssue>,
) {
    if value.is_some_and(str::is_empty) {
        issues.push(issue(path, "Expected a non-empty string."));
    }
}

pub(crate) fn require_unique<T>(values: &[T], path: &str, issues: &mut Vec<ValidationIssue>)
where
    T: Eq + Hash,
{
    let mut seen = HashSet::with_capacity(values.len());
    if values.iter().any(|value| !seen.insert(value)) {
        issues.push(issue(path, "Expected unique items."));
    }
}

pub(crate) fn validate_non_empty_strings(
    values: &[String],
    path: &str,
    require_items: bool,
    issues: &mut Vec<ValidationIssue>,
) {
    if require_items && values.is_empty() {
        issues.push(issue(path, "Expected at least one item."));
    }
    for (index, value) in values.iter().enumerate() {
        require_non_empty(value, &format!("{path}[{index}]"), issues);
    }
    require_unique(values, path, issues);
}

fn json_path(path: &str) -> String {
    if path.is_empty() {
        "$".to_owned()
    } else if path.starts_with('[') {
        format!("${path}")
    } else {
        format!("$.{path}")
    }
}