resource_service.rs 821 lignes · 27939 octets
use crate::error::AppError;
use crate::models::{resource, resource_share};
use crate::services::team_service::TeamService;
use chrono::Utc;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter,
    QueryOrder, Set,
};
use uuid::Uuid;

/// Permission levels for resource sharing (ordered by privilege).
const PERMISSION_HIERARCHY: &[&str] = &["read", "write", "admin"];

/// A share target. Either an individual user or a team — permission
/// resolution walks user-direct shares + every team the user belongs to and
/// keeps the maximum level.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Principal {
    /// An individual user identified by their UUID.
    User(Uuid),
    /// A team identified by its UUID.
    Team(Uuid),
}

/// Service for registering, querying, and sharing application resources.
pub struct ResourceService;

impl ResourceService {
    // -----------------------------------------------------------------
    // Resource registry
    // -----------------------------------------------------------------

    /// Register an application resource in the generic registry.
    ///
    /// Call this after creating a concrete entity (document, project, etc.)
    /// so the framework can manage ownership and sharing.
    pub async fn register(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        owner_id: Uuid,
    ) -> Result<resource::Model, AppError> {
        let model = resource::ActiveModel {
            id: Set(Uuid::new_v4()),
            resource_type: Set(resource_type.to_string()),
            resource_id: Set(resource_id),
            owner_id: Set(owner_id),
            is_public: Set(false),
            created_at: Set(Utc::now()),
        };
        let result = model.insert(db).await?;
        Ok(result)
    }

    /// Look up the registry entry for a concrete resource.
    pub async fn find_by_type_and_id(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
    ) -> Result<Option<resource::Model>, AppError> {
        let entry = resource::Entity::find()
            .filter(resource::Column::ResourceType.eq(resource_type))
            .filter(resource::Column::ResourceId.eq(resource_id))
            .one(db)
            .await?;
        Ok(entry)
    }

    /// Get a registry entry by its internal UUID.
    pub async fn find_by_id(
        db: &DatabaseConnection,
        id: Uuid,
    ) -> Result<Option<resource::Model>, AppError> {
        let entry = resource::Entity::find_by_id(id).one(db).await?;
        Ok(entry)
    }

    /// List all resources of a given type owned by a user.
    pub async fn owned_by_user(
        db: &DatabaseConnection,
        owner_id: Uuid,
        resource_type: &str,
    ) -> Result<Vec<resource::Model>, AppError> {
        let entries = resource::Entity::find()
            .filter(resource::Column::OwnerId.eq(owner_id))
            .filter(resource::Column::ResourceType.eq(resource_type))
            .order_by_desc(resource::Column::CreatedAt)
            .all(db)
            .await?;
        Ok(entries)
    }

    /// Toggle the public visibility flag of a resource.
    pub async fn set_public(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        is_public: bool,
    ) -> Result<resource::Model, AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id)
            .await?
            .ok_or_else(|| AppError::NotFound("Resource not found".into()))?;

        let mut active: resource::ActiveModel = entry.into();
        active.is_public = Set(is_public);
        let updated = active.update(db).await?;
        Ok(updated)
    }

    /// Remove a resource from the registry (cascades to shares).
    pub async fn unregister(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
    ) -> Result<(), AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id).await?;
        if let Some(entry) = entry {
            resource::Entity::delete_by_id(entry.id).exec(db).await?;
        }
        Ok(())
    }

    // -----------------------------------------------------------------
    // Sharing
    // -----------------------------------------------------------------

    /// Share a resource with another user.
    ///
    /// If a share already exists for the same (resource, user), updates the
    /// permission level instead of creating a duplicate.
    pub async fn share(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        shared_with_user_id: Uuid,
        permission_level: &str,
        shared_by_user_id: Uuid,
    ) -> Result<resource_share::Model, AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id)
            .await?
            .ok_or_else(|| AppError::NotFound("Resource not found".into()))?;

        if entry.owner_id == shared_with_user_id {
            return Err(AppError::Validation(
                "Cannot share a resource with its owner".into(),
            ));
        }

        Self::validate_permission_level(permission_level)?;

        let existing = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .filter(resource_share::Column::SharedWithUserId.eq(shared_with_user_id))
            .one(db)
            .await?;

        if let Some(existing) = existing {
            let mut active: resource_share::ActiveModel = existing.into();
            active.permission_level = Set(permission_level.to_string());
            active.shared_by_user_id = Set(shared_by_user_id);
            let updated = active.update(db).await?;
            return Ok(updated);
        }

        let share = resource_share::ActiveModel {
            id: Set(Uuid::new_v4()),
            resource_id: Set(entry.id),
            shared_with_user_id: Set(Some(shared_with_user_id)),
            shared_with_team_id: Set(None),
            shared_with_remote_identity_id: Set(None),
            permission_level: Set(permission_level.to_string()),
            shared_by_user_id: Set(shared_by_user_id),
            created_at: Set(Utc::now()),
        };
        let result = share.insert(db).await?;
        Ok(result)
    }

    /// Share a resource with an arbitrary [`Principal`] (user or team).
    ///
    /// Generalises [`Self::share`] — the legacy helper stays as a thin wrapper
    /// so existing callers keep working. Uniqueness is enforced per
    /// `(resource_id, principal)` via the DB partial unique indexes:
    /// a second call updates the existing row's permission level.
    pub async fn share_with(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        principal: Principal,
        permission_level: &str,
        shared_by_user_id: Uuid,
    ) -> Result<resource_share::Model, AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id)
            .await?
            .ok_or_else(|| AppError::NotFound("Resource not found".into()))?;

        if let Principal::User(uid) = principal {
            if entry.owner_id == uid {
                return Err(AppError::Validation(
                    "Cannot share a resource with its owner".into(),
                ));
            }
        }

        Self::validate_permission_level(permission_level)?;

        let (user_filter, team_filter): (Option<Uuid>, Option<Uuid>) = match principal {
            Principal::User(u) => (Some(u), None),
            Principal::Team(t) => (None, Some(t)),
        };

        let existing = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .filter(
                Condition::all()
                    .add(resource_share::Column::SharedWithUserId.eq(user_filter))
                    .add(resource_share::Column::SharedWithTeamId.eq(team_filter)),
            )
            .one(db)
            .await?;

        if let Some(existing) = existing {
            let mut active: resource_share::ActiveModel = existing.into();
            active.permission_level = Set(permission_level.to_string());
            active.shared_by_user_id = Set(shared_by_user_id);
            return Ok(active.update(db).await?);
        }

        let share = resource_share::ActiveModel {
            id: Set(Uuid::new_v4()),
            resource_id: Set(entry.id),
            shared_with_user_id: Set(user_filter),
            shared_with_team_id: Set(team_filter),
            shared_with_remote_identity_id: Set(None),
            permission_level: Set(permission_level.to_string()),
            shared_by_user_id: Set(shared_by_user_id),
            created_at: Set(Utc::now()),
        };
        Ok(share.insert(db).await?)
    }

    /// Revoke a share by its ID.
    pub async fn revoke_share(db: &DatabaseConnection, share_id: Uuid) -> Result<(), AppError> {
        resource_share::Entity::delete_by_id(share_id)
            .exec(db)
            .await?;
        Ok(())
    }

    /// Revoke all shares for a specific (resource, user) pair.
    pub async fn revoke_user_access(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        user_id: Uuid,
    ) -> Result<(), AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id).await?;
        if let Some(entry) = entry {
            resource_share::Entity::delete_many()
                .filter(resource_share::Column::ResourceId.eq(entry.id))
                .filter(resource_share::Column::SharedWithUserId.eq(user_id))
                .exec(db)
                .await?;
        }
        Ok(())
    }

    /// List all shares for a given resource.
    pub async fn list_shares(
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
    ) -> Result<Vec<resource_share::Model>, AppError> {
        let entry = Self::find_by_type_and_id(db, resource_type, resource_id)
            .await?
            .ok_or_else(|| AppError::NotFound("Resource not found".into()))?;

        let shares = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .order_by_desc(resource_share::Column::CreatedAt)
            .all(db)
            .await?;
        Ok(shares)
    }

    // -----------------------------------------------------------------
    // Access checks
    // -----------------------------------------------------------------

    /// Check whether a user can access a resource at the given permission level.
    ///
    /// Access is granted if any of these conditions is true:
    /// 1. The user is the owner.
    /// 2. The resource is public and `required_level` is `"read"`.
    /// 3. A share exists with a permission level >= `required_level`.
    pub async fn user_can_access(
        db: &DatabaseConnection,
        user_id: Uuid,
        resource_type: &str,
        resource_id: Uuid,
        required_level: &str,
    ) -> Result<bool, AppError> {
        let entry = match Self::find_by_type_and_id(db, resource_type, resource_id).await? {
            Some(e) => e,
            None => return Ok(false),
        };

        if entry.owner_id == user_id {
            return Ok(true);
        }

        if entry.is_public && required_level == "read" {
            return Ok(true);
        }

        let share = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .filter(resource_share::Column::SharedWithUserId.eq(user_id))
            .one(db)
            .await?;

        match share {
            Some(s) => Ok(Self::level_ge(&s.permission_level, required_level)),
            None => Ok(false),
        }
    }

    /// Get the effective permission level a user has on a resource.
    ///
    /// Returns `"owner"` for the owner, the share level if shared,
    /// `"read"` if public, or `None` if no access.
    pub async fn effective_permission(
        db: &DatabaseConnection,
        user_id: Uuid,
        resource_type: &str,
        resource_id: Uuid,
    ) -> Result<Option<String>, AppError> {
        let entry = match Self::find_by_type_and_id(db, resource_type, resource_id).await? {
            Some(e) => e,
            None => return Ok(None),
        };

        if entry.owner_id == user_id {
            return Ok(Some("owner".to_string()));
        }

        let share = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .filter(resource_share::Column::SharedWithUserId.eq(user_id))
            .one(db)
            .await?;

        if let Some(s) = share {
            return Ok(Some(s.permission_level));
        }

        if entry.is_public {
            return Ok(Some("read".to_string()));
        }

        Ok(None)
    }

    /// Compute the effective permission a user has on a resource, walking:
    /// 1. Ownership → `"owner"`.
    /// 2. Direct user share → that share's level.
    /// 3. Every team the user belongs to whose team-share targets the
    ///    resource → take the maximum level across those.
    /// 4. Public flag → `"read"`.
    ///
    /// Returns `None` when the user has no access at all.
    ///
    /// Supersedes [`Self::effective_permission`]; the legacy fn remains for
    /// API compatibility but ignores team memberships.
    pub async fn effective_permission_for_user(
        db: &DatabaseConnection,
        user_id: Uuid,
        resource_type: &str,
        resource_id: Uuid,
    ) -> Result<Option<String>, AppError> {
        let entry = match Self::find_by_type_and_id(db, resource_type, resource_id).await? {
            Some(e) => e,
            None => return Ok(None),
        };

        if entry.owner_id == user_id {
            return Ok(Some("owner".to_string()));
        }

        // Single query pulls both "direct user" and "via team" shares in one
        // round-trip. The DB CHECK guarantees each row is exactly one flavour.
        let team_ids = TeamService::team_ids_of_user(db, user_id).await?;
        let mut condition =
            Condition::any().add(resource_share::Column::SharedWithUserId.eq(Some(user_id)));
        if !team_ids.is_empty() {
            condition = condition.add(resource_share::Column::SharedWithTeamId.is_in(team_ids));
        }

        let shares = resource_share::Entity::find()
            .filter(resource_share::Column::ResourceId.eq(entry.id))
            .filter(condition)
            .all(db)
            .await?;

        let best = shares
            .iter()
            .map(|s| Self::level_rank(&s.permission_level))
            .max();

        if let Some(rank) = best {
            return Ok(Some(PERMISSION_HIERARCHY[rank].to_string()));
        }

        if entry.is_public {
            return Ok(Some("read".to_string()));
        }

        Ok(None)
    }

    /// List all resources of a given type that are shared with a user.
    pub async fn shared_with_user(
        db: &DatabaseConnection,
        user_id: Uuid,
        resource_type: &str,
    ) -> Result<Vec<(resource::Model, resource_share::Model)>, AppError> {
        let shares = resource_share::Entity::find()
            .filter(resource_share::Column::SharedWithUserId.eq(Some(user_id)))
            .all(db)
            .await?;

        let resource_ids: Vec<Uuid> = shares.iter().map(|s| s.resource_id).collect();
        if resource_ids.is_empty() {
            return Ok(vec![]);
        }

        let resources = resource::Entity::find()
            .filter(resource::Column::Id.is_in(resource_ids.clone()))
            .filter(resource::Column::ResourceType.eq(resource_type))
            .all(db)
            .await?;

        let pairs: Vec<(resource::Model, resource_share::Model)> = resources
            .into_iter()
            .filter_map(|r| {
                let share = shares.iter().find(|s| s.resource_id == r.id)?.clone();
                Some((r, share))
            })
            .collect();

        Ok(pairs)
    }

    /// List all resources a user can access (owned + shared + public).
    pub async fn accessible_by_user(
        db: &DatabaseConnection,
        user_id: Uuid,
        resource_type: &str,
    ) -> Result<Vec<resource::Model>, AppError> {
        let owned = Self::owned_by_user(db, user_id, resource_type).await?;
        let shared: Vec<resource::Model> = Self::shared_with_user(db, user_id, resource_type)
            .await?
            .into_iter()
            .map(|(r, _)| r)
            .collect();

        let public = resource::Entity::find()
            .filter(resource::Column::ResourceType.eq(resource_type))
            .filter(resource::Column::IsPublic.eq(true))
            .all(db)
            .await?;

        let mut all = owned;
        for r in shared.into_iter().chain(public) {
            if !all.iter().any(|existing| existing.id == r.id) {
                all.push(r);
            }
        }

        Ok(all)
    }

    // -----------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------

    fn validate_permission_level(level: &str) -> Result<(), AppError> {
        if PERMISSION_HIERARCHY.contains(&level) {
            Ok(())
        } else {
            Err(AppError::Validation(format!(
                "Invalid permission level '{}'. Must be one of: {}",
                level,
                PERMISSION_HIERARCHY.join(", ")
            )))
        }
    }

    /// Returns true if `granted` >= `required` in the hierarchy.
    fn level_ge(granted: &str, required: &str) -> bool {
        let granted_idx = PERMISSION_HIERARCHY
            .iter()
            .position(|&l| l == granted)
            .unwrap_or(0);
        let required_idx = PERMISSION_HIERARCHY
            .iter()
            .position(|&l| l == required)
            .unwrap_or(usize::MAX);
        granted_idx >= required_idx
    }

    /// Numeric rank of a permission level (higher = more privileged). Unknown
    /// levels fall back to 0 so they are never preferred over a known one.
    fn level_rank(level: &str) -> usize {
        PERMISSION_HIERARCHY
            .iter()
            .position(|&l| l == level)
            .unwrap_or(0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::services::password_service::PasswordService;
    use crate::services::team_service::{TeamRole, TeamService};
    use crate::services::user_service::UserService;
    use crate::types::TeamSlug;

    async fn test_db() -> DatabaseConnection {
        dotenvy::dotenv().ok();
        let url = std::env::var("DATABASE_URL")
            .expect("DATABASE_URL required for resource_service tests");
        sea_orm::Database::connect(&url).await.unwrap()
    }

    async fn fresh_user(db: &DatabaseConnection) -> Uuid {
        let unique = Uuid::new_v4().simple().to_string();
        let hash = PasswordService::hash_password("IrrelevantForTests1!")
            .await
            .unwrap();
        UserService::create_user(
            db,
            format!("res-test-{}", &unique[..12]),
            format!("res_test_{}@example.com", &unique[..12]),
            hash,
            false,
        )
        .await
        .unwrap()
        .id
    }

    fn fresh_slug() -> TeamSlug {
        let raw = format!("rs-{}", &Uuid::new_v4().simple().to_string()[..10]);
        TeamSlug::parse(&raw).unwrap()
    }

    async fn register_resource(db: &DatabaseConnection, owner: Uuid) -> Uuid {
        let rid = Uuid::new_v4();
        ResourceService::register(db, "test_resource", rid, owner)
            .await
            .unwrap();
        rid
    }

    #[tokio::test]
    async fn share_with_user_principal_is_backwards_compatible() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let alice = fresh_user(&db).await;
        let rid = register_resource(&db, owner).await;

        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::User(alice),
            "write",
            owner,
        )
        .await
        .unwrap();

        let perm = ResourceService::effective_permission_for_user(&db, alice, "test_resource", rid)
            .await
            .unwrap();
        assert_eq!(perm.as_deref(), Some("write"));
    }

    #[tokio::test]
    async fn share_with_user_refuses_sharing_back_to_owner() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let rid = register_resource(&db, owner).await;
        let err = ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::User(owner),
            "read",
            owner,
        )
        .await
        .unwrap_err();
        assert!(matches!(err, AppError::Validation(_)));
    }

    #[tokio::test]
    async fn share_with_team_grants_access_to_every_member() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let alice = fresh_user(&db).await;
        let bob = fresh_user(&db).await;
        let team = TeamService::create(&db, owner, fresh_slug(), "Devs", None)
            .await
            .unwrap();
        TeamService::add_member(&db, team.id, alice, TeamRole::Member, None)
            .await
            .unwrap();
        TeamService::add_member(&db, team.id, bob, TeamRole::Member, None)
            .await
            .unwrap();

        let rid = register_resource(&db, owner).await;
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team.id),
            "read",
            owner,
        )
        .await
        .unwrap();

        let alice_perm =
            ResourceService::effective_permission_for_user(&db, alice, "test_resource", rid)
                .await
                .unwrap();
        let bob_perm =
            ResourceService::effective_permission_for_user(&db, bob, "test_resource", rid)
                .await
                .unwrap();
        assert_eq!(alice_perm.as_deref(), Some("read"));
        assert_eq!(bob_perm.as_deref(), Some("read"));
    }

    #[tokio::test]
    async fn effective_permission_takes_max_across_user_and_teams() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let bob = fresh_user(&db).await;
        let team_read = TeamService::create(&db, owner, fresh_slug(), "Read", None)
            .await
            .unwrap();
        let team_admin = TeamService::create(&db, owner, fresh_slug(), "Admin", None)
            .await
            .unwrap();
        TeamService::add_member(&db, team_read.id, bob, TeamRole::Member, None)
            .await
            .unwrap();
        TeamService::add_member(&db, team_admin.id, bob, TeamRole::Member, None)
            .await
            .unwrap();

        let rid = register_resource(&db, owner).await;
        // Bob gets "write" directly.
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::User(bob),
            "write",
            owner,
        )
        .await
        .unwrap();
        // "Read" team only has read.
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team_read.id),
            "read",
            owner,
        )
        .await
        .unwrap();
        // "Admin" team has admin → should win.
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team_admin.id),
            "admin",
            owner,
        )
        .await
        .unwrap();

        let perm = ResourceService::effective_permission_for_user(&db, bob, "test_resource", rid)
            .await
            .unwrap();
        assert_eq!(perm.as_deref(), Some("admin"));
    }

    #[tokio::test]
    async fn removing_user_from_team_drops_team_based_access() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let bob = fresh_user(&db).await;
        let team = TeamService::create(&db, owner, fresh_slug(), "Backend", None)
            .await
            .unwrap();
        TeamService::add_member(&db, team.id, bob, TeamRole::Member, None)
            .await
            .unwrap();

        let rid = register_resource(&db, owner).await;
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team.id),
            "write",
            owner,
        )
        .await
        .unwrap();

        // Sanity: bob has write via team.
        assert_eq!(
            ResourceService::effective_permission_for_user(&db, bob, "test_resource", rid)
                .await
                .unwrap()
                .as_deref(),
            Some("write")
        );

        TeamService::remove_member(&db, team.id, bob, None)
            .await
            .unwrap();

        // After removal bob has no access.
        assert_eq!(
            ResourceService::effective_permission_for_user(&db, bob, "test_resource", rid)
                .await
                .unwrap(),
            None
        );
    }

    #[tokio::test]
    async fn share_with_team_is_idempotent_and_updates_level() {
        let db = test_db().await;
        let owner = fresh_user(&db).await;
        let bob = fresh_user(&db).await;
        let team = TeamService::create(&db, owner, fresh_slug(), "T", None)
            .await
            .unwrap();
        TeamService::add_member(&db, team.id, bob, TeamRole::Member, None)
            .await
            .unwrap();
        let rid = register_resource(&db, owner).await;

        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team.id),
            "read",
            owner,
        )
        .await
        .unwrap();
        ResourceService::share_with(
            &db,
            "test_resource",
            rid,
            Principal::Team(team.id),
            "write",
            owner,
        )
        .await
        .unwrap();

        // Exactly one share row — second call upserted.
        let shares = ResourceService::list_shares(&db, "test_resource", rid)
            .await
            .unwrap();
        let team_shares: Vec<_> = shares
            .iter()
            .filter(|s| s.shared_with_team_id == Some(team.id))
            .collect();
        assert_eq!(team_shares.len(), 1);
        assert_eq!(team_shares[0].permission_level, "write");
    }

    #[test]
    fn test_level_ge() {
        assert!(ResourceService::level_ge("admin", "read"));
        assert!(ResourceService::level_ge("admin", "write"));
        assert!(ResourceService::level_ge("admin", "admin"));
        assert!(ResourceService::level_ge("write", "read"));
        assert!(ResourceService::level_ge("write", "write"));
        assert!(!ResourceService::level_ge("write", "admin"));
        assert!(ResourceService::level_ge("read", "read"));
        assert!(!ResourceService::level_ge("read", "write"));
        assert!(!ResourceService::level_ge("read", "admin"));
    }

    #[test]
    fn test_validate_permission_level() {
        assert!(ResourceService::validate_permission_level("read").is_ok());
        assert!(ResourceService::validate_permission_level("write").is_ok());
        assert!(ResourceService::validate_permission_level("admin").is_ok());
        assert!(ResourceService::validate_permission_level("superadmin").is_err());
        assert!(ResourceService::validate_permission_level("").is_err());
    }
}