alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Shared owner-boundary operation diagnostics.

use crate::fs_utils::{FileReadError, FileWriteError};
use std::io;

/// Coarse operation failure class.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationFailureClass {
    /// The source error is already a stable user-facing status contract.
    UserVisible,
    /// Persistence failed below the owner boundary.
    Persistence,
    /// Required runtime configuration is unavailable.
    RuntimeConfig,
}

/// Retry guidance for the same already-validated operation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationRetryAdvice {
    /// Retrying the same request may succeed.
    Retry,
    /// Retrying needs a user, config, path, or environment change first.
    RetryAfterChange,
    /// The lower-boundary source is not specific enough to decide.
    Unknown,
}

/// Redacted lower-boundary source shape.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperationFailureSource {
    /// Path policy rejected the operation.
    PathPolicy,
    /// The path did not resolve to an existing file.
    Missing,
    /// Metadata could not be read.
    Metadata {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// The path is not a regular file.
    UnsupportedFileType,
    /// The file exceeds the configured size cap.
    TooLarge,
    /// Opening the file failed.
    Open {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// Reading the file failed.
    Read {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// The target path has no parent directory.
    MissingParent,
    /// Creating the temporary file failed.
    CreateTemp {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// Writing to the temporary file failed.
    WriteTemp {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// Flushing a file or directory failed.
    SyncTemp {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// Applying permissions to the temporary file failed.
    SetTempPermissions {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
    /// Renaming the temporary file into place failed.
    Rename {
        /// I/O error kind without path or OS-specific message text.
        kind: io::ErrorKind,
    },
}

impl OperationFailureSource {
    /// Builds a redacted source shape from a filesystem read failure.
    #[must_use]
    pub fn from_file_read_error(error: &FileReadError) -> Self {
        match error {
            FileReadError::Policy(_error) => Self::PathPolicy,
            FileReadError::Missing { .. } => Self::Missing,
            FileReadError::Metadata { source, .. } => Self::Metadata {
                kind: source.kind(),
            },
            FileReadError::UnsupportedFileType { .. } => Self::UnsupportedFileType,
            FileReadError::TooLarge { .. } => Self::TooLarge,
            FileReadError::Open { source, .. } => Self::Open {
                kind: source.kind(),
            },
            FileReadError::Read { source, .. } => Self::Read {
                kind: source.kind(),
            },
        }
    }

    /// Builds a redacted source shape from a filesystem write failure.
    #[must_use]
    pub fn from_file_write_error(error: &FileWriteError) -> Self {
        match error {
            FileWriteError::Policy(_error) => Self::PathPolicy,
            FileWriteError::Metadata { source, .. } => Self::Metadata {
                kind: source.kind(),
            },
            FileWriteError::UnsupportedFileType { .. } => Self::UnsupportedFileType,
            FileWriteError::MissingParent { .. } => Self::MissingParent,
            FileWriteError::CreateTemp { source, .. } => Self::CreateTemp {
                kind: source.kind(),
            },
            FileWriteError::WriteTemp { source, .. } => Self::WriteTemp {
                kind: source.kind(),
            },
            FileWriteError::SyncTemp { source, .. } => Self::SyncTemp {
                kind: source.kind(),
            },
            FileWriteError::SetTempPermissions { source, .. } => Self::SetTempPermissions {
                kind: source.kind(),
            },
            FileWriteError::Rename { source, .. } => Self::Rename {
                kind: source.kind(),
            },
        }
    }

    /// Returns retry guidance without exposing paths or OS messages.
    #[must_use]
    pub const fn retry_advice(self) -> OperationRetryAdvice {
        match self {
            Self::PathPolicy
            | Self::Missing
            | Self::UnsupportedFileType
            | Self::TooLarge
            | Self::MissingParent => OperationRetryAdvice::RetryAfterChange,
            Self::Metadata { kind }
            | Self::Open { kind }
            | Self::Read { kind }
            | Self::CreateTemp { kind }
            | Self::WriteTemp { kind }
            | Self::SyncTemp { kind }
            | Self::SetTempPermissions { kind }
            | Self::Rename { kind } => retry_advice_for_io_kind(kind),
        }
    }
}

/// Maps redacted I/O classes into same-request retry guidance.
const fn retry_advice_for_io_kind(kind: io::ErrorKind) -> OperationRetryAdvice {
    match kind {
        io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
            OperationRetryAdvice::Retry
        }
        io::ErrorKind::NotFound
        | io::ErrorKind::PermissionDenied
        | io::ErrorKind::AlreadyExists
        | io::ErrorKind::InvalidInput
        | io::ErrorKind::InvalidData
        | io::ErrorKind::WriteZero
        | io::ErrorKind::Unsupported => OperationRetryAdvice::RetryAfterChange,
        _ => OperationRetryAdvice::Unknown,
    }
}

#[cfg(test)]
mod tests {
    use super::{OperationFailureSource, OperationRetryAdvice};
    use crate::fs_utils::{FileReadError, FileWriteError};
    use std::{io, path::PathBuf};

    #[test]
    fn read_failures_map_to_redacted_source_shapes() {
        let path = PathBuf::from("secret/path.txt");
        let missing = FileReadError::Missing { path: path.clone() };
        assert_eq!(
            OperationFailureSource::from_file_read_error(&missing),
            OperationFailureSource::Missing
        );

        let open = FileReadError::Open {
            path: path.clone(),
            source: io::Error::from(io::ErrorKind::PermissionDenied),
        };
        assert_eq!(
            OperationFailureSource::from_file_read_error(&open),
            OperationFailureSource::Open {
                kind: io::ErrorKind::PermissionDenied
            }
        );

        let too_large = FileReadError::TooLarge {
            path,
            size: 2048,
            max_size: 1024,
        };
        assert_eq!(
            OperationFailureSource::from_file_read_error(&too_large),
            OperationFailureSource::TooLarge
        );
    }

    #[test]
    fn write_failures_map_to_redacted_source_shapes() {
        let error = FileWriteError::Rename {
            temp_path: PathBuf::from("secret/.tmp"),
            target_path: PathBuf::from("secret/path.txt"),
            source: io::Error::from(io::ErrorKind::TimedOut),
        };

        assert_eq!(
            OperationFailureSource::from_file_write_error(&error),
            OperationFailureSource::Rename {
                kind: io::ErrorKind::TimedOut
            }
        );
    }

    #[test]
    fn retry_advice_is_closed_over_redacted_shapes() {
        assert_eq!(
            OperationFailureSource::Missing.retry_advice(),
            OperationRetryAdvice::RetryAfterChange
        );
        assert_eq!(
            OperationFailureSource::Read {
                kind: io::ErrorKind::TimedOut,
            }
            .retry_advice(),
            OperationRetryAdvice::Retry
        );
        assert_eq!(
            OperationFailureSource::UnsupportedFileType.retry_advice(),
            OperationRetryAdvice::RetryAfterChange
        );
    }
}