uptrakit-web-api-types 0.0.4

Shared HTTP request/response types for the Uptrakit web API
Documentation
use crate::validation::{Validate, ValidationError};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// A user with their assigned roles.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UserWithRolesResponse {
    pub id: Uuid,
    pub email: String,
    pub first_name: String,
    pub last_name: String,
    pub is_active: bool,
    pub roles: Vec<UserRoleSummary>,
}

/// Summary of a role assigned to a user.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UserRoleSummary {
    pub id: Uuid,
    pub name: String,
}

/// Request to replace a user's roles.
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateUserRolesRequest {
    /// List of role IDs to assign. Replaces all existing role assignments.
    pub role_ids: Vec<Uuid>,
}

impl Validate for UpdateUserRolesRequest {
    fn validate(&self) -> Result<(), ValidationError> {
        if self.role_ids.is_empty() {
            return Err(ValidationError {
                field: "role_ids",
                message: "at least one role must be assigned".to_string(),
            });
        }
        if self.role_ids.len() > 20 {
            return Err(ValidationError {
                field: "role_ids",
                message: "cannot assign more than 20 roles".to_string(),
            });
        }
        Ok(())
    }
}

/// Request to activate or deactivate a user.
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateUserActiveRequest {
    pub is_active: bool,
}

impl Validate for UpdateUserActiveRequest {
    fn validate(&self) -> Result<(), ValidationError> {
        // No format/length invariants beyond field types; capability/existence checks are handler-side.
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    #![expect(
        clippy::assertions_on_result_states,
        reason = "test assertions — is_ok/is_err provides readable failure messages"
    )]
    use super::*;

    #[test]
    fn update_user_active_validate_is_ok() {
        assert!(
            UpdateUserActiveRequest { is_active: false }
                .validate()
                .is_ok()
        );
    }
}