soaprs-http 0.1.0

Transport-neutral HTTP metadata for soaprs
Documentation
//! Endpoint metadata without framework handler types.

use std::time::Duration;

use http::Method;
use soaprs_core::{SoapError, SoapResult};

use crate::{AuthorizationPolicy, RateLimitPolicy, RoutePath};

/// Portable endpoint metadata consumed by framework adapters and documentation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EndpointMetadata {
    /// Stable endpoint name used by diagnostics and API documentation.
    pub name: String,
    /// HTTP method.
    pub method: Method,
    /// Portable route path.
    pub path: RoutePath,
    /// Authorization requirement.
    pub authorization: AuthorizationPolicy,
    /// Optional rate-limit policy.
    pub rate_limit: Option<RateLimitPolicy>,
    /// Optional request timeout.
    pub timeout: Option<Duration>,
    /// Documentation and grouping tags.
    pub tags: Vec<String>,
}

impl EndpointMetadata {
    /// Creates endpoint metadata with public access and no optional policies.
    pub fn new(name: impl Into<String>, method: Method, path: RoutePath) -> SoapResult<Self> {
        let name = name.into();
        if name.is_empty()
            || !name.chars().all(|character| {
                character == '.' || character == '_' || character.is_ascii_alphanumeric()
            })
        {
            return Err(SoapError::validation(format!(
                "invalid endpoint name `{name}`"
            )));
        }
        Ok(Self {
            name,
            method,
            path,
            authorization: AuthorizationPolicy::Public,
            rate_limit: None,
            timeout: None,
            tags: Vec::new(),
        })
    }

    /// Sets the authorization policy.
    pub fn authorize(mut self, policy: AuthorizationPolicy) -> Self {
        self.authorization = policy;
        self
    }

    /// Sets the rate-limit policy.
    pub fn rate_limit(mut self, policy: RateLimitPolicy) -> Self {
        self.rate_limit = Some(policy);
        self
    }

    /// Sets a non-zero request timeout.
    pub fn timeout(mut self, timeout: Duration) -> SoapResult<Self> {
        if timeout.is_zero() {
            return Err(SoapError::validation(
                "endpoint timeout must be greater than zero",
            ));
        }
        self.timeout = Some(timeout);
        Ok(self)
    }

    /// Adds a non-empty documentation tag once.
    pub fn tag(mut self, tag: impl Into<String>) -> SoapResult<Self> {
        let tag = tag.into();
        if tag.trim().is_empty() {
            return Err(SoapError::validation("endpoint tag cannot be empty"));
        }
        if !self.tags.contains(&tag) {
            self.tags.push(tag);
        }
        Ok(self)
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use http::Method;

    use crate::{AuthorizationPolicy, EndpointMetadata, RoutePath};

    #[test]
    fn builds_metadata_without_a_framework_handler() {
        let result = EndpointMetadata::new(
            "users.get",
            Method::GET,
            match RoutePath::new("/users/{id}") {
                Ok(path) => path,
                Err(error) => panic!("valid path failed: {error}"),
            },
        )
        .map(|metadata| metadata.authorize(AuthorizationPolicy::Authenticated))
        .and_then(|metadata| metadata.timeout(Duration::from_secs(5)))
        .and_then(|metadata| metadata.tag("users"));

        assert!(result.is_ok());
    }
}