synta-python 0.3.0

Python extension module for the synta ASN.1 library
Documentation
//! Python bindings for building X.509 Distinguished Names.
//!
//! Wraps [`synta_certificate::NameBuilder`], a fluent builder that accumulates
//! ``AttributeTypeAndValue`` entries and serialises them into a DER-encoded
//! ``Name`` SEQUENCE on demand.
//!
//! # Chaining
//!
//! Each setter returns the same ``NameBuilder`` object, enabling a
//! single-expression build:
//!
//! ```python,ignore
//! import synta
//!
//! name_der = (
//!     synta.NameBuilder()
//!     .country("US")
//!     .organization("Example Corp")
//!     .common_name("example.com")
//!     .build()
//! )
//! ```

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use synta_certificate::NameBuilder;

// ── NameBuilder ───────────────────────────────────────────────────────────────

/// Builder for DER-encoded X.509 ``Name`` SEQUENCE structures.
///
/// Constructs a ``Name`` by accumulating ``RelativeDistinguishedName`` (RDN)
/// entries, each containing one ``AttributeTypeAndValue`` (ATV) with a
/// ``UTF8String`` value.  Call :meth:`build` at the end to obtain the complete
/// DER bytes.
///
/// The output is a complete DER ``Name`` TLV suitable for use with
/// :class:`CertificateBuilder` and :class:`CsrBuilder` methods
/// ``issuer_name``, ``subject_name``.
///
/// Each setter method returns the **same** ``NameBuilder`` object, so calls
/// can be chained with ``.``:
///
/// ```python,ignore
/// import synta
///
/// # CN only
/// name_der = synta.NameBuilder().common_name("My Root CA").build()
///
/// # Full distinguished name
/// name_der = (
///     synta.NameBuilder()
///     .country("US")
///     .organization("Example Corp")
///     .organizational_unit("Engineering")
///     .common_name("example.com")
///     .build()
/// )
///
/// # Use with CertificateBuilder:
/// cert = (
///     synta.CertificateBuilder()
///     .issuer_name(
///         synta.NameBuilder().common_name("Test CA").build()
///     )
///     .subject_name(
///         synta.NameBuilder().common_name("Test Leaf").build()
///     )
///     ...
/// )
/// ```
#[pyclass(name = "NameBuilder")]
pub struct PyNameBuilder {
    inner: NameBuilder,
}

#[pymethods]
impl PyNameBuilder {
    /// Create a new, empty ``NameBuilder``.
    #[new]
    fn new() -> Self {
        Self {
            inner: NameBuilder::new(),
        }
    }

    /// Add an attribute with a dotted-decimal OID string and a UTF-8 string value.
    ///
    /// Use the constants in ``synta.oids.attr`` for well-known DN attribute types,
    /// or pass any OID string for custom attributes.
    ///
    /// ```python,ignore
    /// import synta
    /// import synta.oids as oids
    ///
    /// name_der = (
    ///     synta.NameBuilder()
    ///     .add_attr(str(oids.attr.ORGANIZATION), "Example Corp")
    ///     .add_attr(str(oids.attr.COMMON_NAME), "example.com")
    ///     .build()
    /// )
    /// ```
    fn add_attr<'py>(slf: Bound<'py, Self>, oid: &str, value: &str) -> PyResult<Bound<'py, Self>> {
        use std::str::FromStr;
        let parsed = synta::ObjectIdentifier::from_str(oid)
            .map_err(|_| PyValueError::new_err(format!("invalid OID: {oid}")))?;
        let comps = parsed.components().to_vec();
        // Replace inner via a consuming take-and-rebuild pattern.
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.add_attr(&comps, value);
        Ok(slf)
    }

    /// Add a ``commonName`` (2.5.4.3) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().common_name("example.com").build()
    /// ```
    fn common_name<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.common_name(value);
        slf
    }

    /// Add an ``organizationName`` (2.5.4.10) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().organization("Example Corp").build()
    /// ```
    fn organization<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.organization(value);
        slf
    }

    /// Add an ``organizationalUnitName`` (2.5.4.11) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().organizational_unit("Engineering").build()
    /// ```
    fn organizational_unit<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.organizational_unit(value);
        slf
    }

    /// Add a ``countryName`` (2.5.4.6) attribute.
    ///
    /// The value should be a two-letter ISO 3166-1 country code (e.g. ``"US"``).
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().country("US").build()
    /// ```
    fn country<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.country(value);
        slf
    }

    /// Add a ``stateOrProvinceName`` (2.5.4.8) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().state("California").build()
    /// ```
    fn state<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.state(value);
        slf
    }

    /// Add a ``localityName`` (2.5.4.7) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().locality("San Francisco").build()
    /// ```
    fn locality<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.locality(value);
        slf
    }

    /// Add a ``streetAddress`` (2.5.4.9) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().street("123 Main St").build()
    /// ```
    fn street<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.street(value);
        slf
    }

    /// Add an ``emailAddress`` (1.2.840.113549.1.9.1) attribute.
    ///
    /// ```python,ignore
    /// name_der = synta.NameBuilder().email_address("admin@example.com").build()
    /// ```
    fn email_address<'py>(slf: Bound<'py, Self>, value: &str) -> Bound<'py, Self> {
        let old = std::mem::replace(&mut slf.borrow_mut().inner, NameBuilder::new());
        slf.borrow_mut().inner = old.email_address(value);
        slf
    }

    /// Build and return the DER-encoded X.509 ``Name`` SEQUENCE as ``bytes``.
    ///
    /// Returns the complete TLV bytes including the outer ``SEQUENCE`` tag and
    /// length.  An empty ``NameBuilder`` returns a DER-encoded empty
    /// ``SEQUENCE`` (``b'\\x30\\x00'``).
    ///
    /// ```python,ignore
    /// import synta
    ///
    /// name_der = synta.NameBuilder().common_name("Root CA").build()
    ///
    /// # Use directly with CertificateBuilder:
    /// cert = (
    ///     synta.CertificateBuilder()
    ///     .issuer_name(name_der)
    ///     .subject_name(name_der)
    ///     ...
    /// )
    /// ```
    fn build<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let der = self
            .inner
            .build()
            .map_err(pyo3::exceptions::PyValueError::new_err)?;
        Ok(PyBytes::new(py, &der))
    }

    fn __repr__(&self) -> String {
        "NameBuilder()".to_string()
    }
}