canaad-core 0.2.1

Core library for AAD canonicalization per RFC 8785
Documentation
//! Builder for constructing an `AadContext` with deferred validation.

use crate::context::AadContext;
use crate::error::AadError;
use crate::types::ExtensionValue;

/// Unvalidated extension value. Converted in `build()`.
#[derive(Debug)]
pub(super) enum RawExtValue {
    String(String),
    Integer(u64),
}

/// Builder for constructing an `AadContext`.
///
/// All validation is deferred to `build()`. Setters accept raw values and store
/// them without validation. Use `AadContext::builder()` to obtain an instance.
#[derive(Debug, Default)]
pub struct AadContextBuilder {
    pub(super) tenant: Option<String>,
    pub(super) resource: Option<String>,
    pub(super) purpose: Option<String>,
    pub(super) timestamp: Option<u64>,
    pub(super) extensions: Vec<(String, RawExtValue)>,
}

impl AadContextBuilder {
    /// Creates a new builder.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the tenant identifier.
    #[must_use]
    pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
        self.tenant = Some(tenant.into());
        self
    }

    /// Sets the resource path.
    #[must_use]
    pub fn resource(mut self, resource: impl Into<String>) -> Self {
        self.resource = Some(resource.into());
        self
    }

    /// Sets the purpose.
    #[must_use]
    pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
        self.purpose = Some(purpose.into());
        self
    }

    /// Sets the timestamp.
    #[must_use]
    pub const fn timestamp(mut self, ts: u64) -> Self {
        self.timestamp = Some(ts);
        self
    }

    /// Adds a string extension field.
    #[must_use]
    pub fn extension_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.extensions.push((key.into(), RawExtValue::String(value.into())));
        self
    }

    /// Adds an integer extension field.
    #[must_use]
    pub fn extension_int(mut self, key: impl Into<String>, value: u64) -> Self {
        self.extensions.push((key.into(), RawExtValue::Integer(value)));
        self
    }

    /// Builds the `AadContext`, validating all fields.
    ///
    /// # Errors
    ///
    /// Returns an error if any required field is missing or any value is invalid.
    pub fn build(self) -> Result<AadContext, AadError> {
        let tenant =
            self.tenant.ok_or(AadError::MissingRequiredField { field: "tenant" })?;
        let resource =
            self.resource.ok_or(AadError::MissingRequiredField { field: "resource" })?;
        let purpose =
            self.purpose.ok_or(AadError::MissingRequiredField { field: "purpose" })?;

        let mut ctx = AadContext::new(tenant, resource, purpose)?;

        if let Some(ts) = self.timestamp {
            ctx = ctx.with_timestamp(ts)?;
        }

        for (key, raw) in self.extensions {
            let value = match raw {
                RawExtValue::String(s) => ExtensionValue::string(s)?,
                RawExtValue::Integer(i) => ExtensionValue::integer(i)?,
            };
            ctx = ctx.with_extension(key, value)?;
        }

        Ok(ctx)
    }
}