rusticity-term 0.1.6

Terminal UI components for Rusticity
Documentation
use crate::common::{
    format_bytes, format_duration_seconds, format_unix_timestamp, translate_column, ColumnId,
    UTC_TIMESTAMP_WIDTH,
};
use crate::ui::table::Column as TableColumn;
use ratatui::prelude::*;
use std::collections::HashMap;

pub fn init(i18n: &mut HashMap<String, String>) {
    for col in Column::all() {
        i18n.entry(col.id().to_string())
            .or_insert_with(|| col.default_name().to_string());
    }
}

#[derive(Debug, Clone)]
pub struct Queue {
    pub name: String,
    pub url: String,
    pub queue_type: String,
    pub created_timestamp: String,
    pub messages_available: String,
    pub messages_in_flight: String,
    pub encryption: String,
    pub content_based_deduplication: String,
    pub last_modified_timestamp: String,
    pub visibility_timeout: String,
    pub message_retention_period: String,
    pub maximum_message_size: String,
    pub delivery_delay: String,
    pub receive_message_wait_time: String,
    pub high_throughput_fifo: String,
    pub deduplication_scope: String,
    pub fifo_throughput_limit: String,
    pub dead_letter_queue: String,
    pub messages_delayed: String,
    pub redrive_allow_policy: String,
    pub redrive_policy: String,
    pub redrive_task_id: String,
    pub redrive_task_start_time: String,
    pub redrive_task_status: String,
    pub redrive_task_percent: String,
    pub redrive_task_destination: String,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Column {
    Name,
    Type,
    Created,
    MessagesAvailable,
    MessagesInFlight,
    Encryption,
    ContentBasedDeduplication,
    LastUpdated,
    VisibilityTimeout,
    MessageRetentionPeriod,
    MaximumMessageSize,
    DeliveryDelay,
    ReceiveMessageWaitTime,
    HighThroughputFifo,
    DeduplicationScope,
    FifoThroughputLimit,
}

impl Column {
    const ID_NAME: &'static str = "column.sqs.queue.name";
    const ID_TYPE: &'static str = "column.sqs.queue.type";
    const ID_CREATED: &'static str = "column.sqs.queue.created";
    const ID_MESSAGES_AVAILABLE: &'static str = "column.sqs.queue.messages_available";
    const ID_MESSAGES_IN_FLIGHT: &'static str = "column.sqs.queue.messages_in_flight";
    const ID_ENCRYPTION: &'static str = "column.sqs.queue.encryption";
    const ID_CONTENT_BASED_DEDUPLICATION: &'static str =
        "column.sqs.queue.content_based_deduplication";
    const ID_LAST_UPDATED: &'static str = "column.sqs.queue.last_updated";
    const ID_VISIBILITY_TIMEOUT: &'static str = "column.sqs.queue.visibility_timeout";
    const ID_MESSAGE_RETENTION_PERIOD: &'static str = "column.sqs.queue.message_retention_period";
    const ID_MAXIMUM_MESSAGE_SIZE: &'static str = "column.sqs.queue.maximum_message_size";
    const ID_DELIVERY_DELAY: &'static str = "column.sqs.queue.delivery_delay";
    const ID_RECEIVE_MESSAGE_WAIT_TIME: &'static str = "column.sqs.queue.receive_message_wait_time";
    const ID_HIGH_THROUGHPUT_FIFO: &'static str = "column.sqs.queue.high_throughput_fifo";
    const ID_DEDUPLICATION_SCOPE: &'static str = "column.sqs.queue.deduplication_scope";
    const ID_FIFO_THROUGHPUT_LIMIT: &'static str = "column.sqs.queue.fifo_throughput_limit";

    pub const fn id(&self) -> ColumnId {
        match self {
            Column::Name => Self::ID_NAME,
            Column::Type => Self::ID_TYPE,
            Column::Created => Self::ID_CREATED,
            Column::MessagesAvailable => Self::ID_MESSAGES_AVAILABLE,
            Column::MessagesInFlight => Self::ID_MESSAGES_IN_FLIGHT,
            Column::Encryption => Self::ID_ENCRYPTION,
            Column::ContentBasedDeduplication => Self::ID_CONTENT_BASED_DEDUPLICATION,
            Column::LastUpdated => Self::ID_LAST_UPDATED,
            Column::VisibilityTimeout => Self::ID_VISIBILITY_TIMEOUT,
            Column::MessageRetentionPeriod => Self::ID_MESSAGE_RETENTION_PERIOD,
            Column::MaximumMessageSize => Self::ID_MAXIMUM_MESSAGE_SIZE,
            Column::DeliveryDelay => Self::ID_DELIVERY_DELAY,
            Column::ReceiveMessageWaitTime => Self::ID_RECEIVE_MESSAGE_WAIT_TIME,
            Column::HighThroughputFifo => Self::ID_HIGH_THROUGHPUT_FIFO,
            Column::DeduplicationScope => Self::ID_DEDUPLICATION_SCOPE,
            Column::FifoThroughputLimit => Self::ID_FIFO_THROUGHPUT_LIMIT,
        }
    }

    pub const fn default_name(&self) -> &'static str {
        match self {
            Column::Name => "Name",
            Column::Type => "Type",
            Column::Created => "Created",
            Column::MessagesAvailable => "Messages available",
            Column::MessagesInFlight => "Messages in flight",
            Column::Encryption => "Encryption",
            Column::ContentBasedDeduplication => "Content-based deduplication",
            Column::LastUpdated => "Last updated",
            Column::VisibilityTimeout => "Visibility timeout",
            Column::MessageRetentionPeriod => "Message retention period",
            Column::MaximumMessageSize => "Maximum message size",
            Column::DeliveryDelay => "Delivery delay",
            Column::ReceiveMessageWaitTime => "Receive message wait time",
            Column::HighThroughputFifo => "High throughput FIFO",
            Column::DeduplicationScope => "Deduplication scope",
            Column::FifoThroughputLimit => "FIFO throughput limit",
        }
    }

    pub fn name(&self) -> String {
        translate_column(self.id(), self.default_name())
    }

    pub fn from_id(id: &str) -> Option<Self> {
        match id {
            Self::ID_NAME => Some(Column::Name),
            Self::ID_TYPE => Some(Column::Type),
            Self::ID_CREATED => Some(Column::Created),
            Self::ID_MESSAGES_AVAILABLE => Some(Column::MessagesAvailable),
            Self::ID_MESSAGES_IN_FLIGHT => Some(Column::MessagesInFlight),
            Self::ID_ENCRYPTION => Some(Column::Encryption),
            Self::ID_CONTENT_BASED_DEDUPLICATION => Some(Column::ContentBasedDeduplication),
            Self::ID_LAST_UPDATED => Some(Column::LastUpdated),
            Self::ID_VISIBILITY_TIMEOUT => Some(Column::VisibilityTimeout),
            Self::ID_MESSAGE_RETENTION_PERIOD => Some(Column::MessageRetentionPeriod),
            Self::ID_MAXIMUM_MESSAGE_SIZE => Some(Column::MaximumMessageSize),
            Self::ID_DELIVERY_DELAY => Some(Column::DeliveryDelay),
            Self::ID_RECEIVE_MESSAGE_WAIT_TIME => Some(Column::ReceiveMessageWaitTime),
            Self::ID_HIGH_THROUGHPUT_FIFO => Some(Column::HighThroughputFifo),
            Self::ID_DEDUPLICATION_SCOPE => Some(Column::DeduplicationScope),
            Self::ID_FIFO_THROUGHPUT_LIMIT => Some(Column::FifoThroughputLimit),
            _ => None,
        }
    }

    pub fn all() -> [Column; 16] {
        [
            Column::Name,
            Column::Type,
            Column::Created,
            Column::MessagesAvailable,
            Column::MessagesInFlight,
            Column::Encryption,
            Column::ContentBasedDeduplication,
            Column::LastUpdated,
            Column::VisibilityTimeout,
            Column::MessageRetentionPeriod,
            Column::MaximumMessageSize,
            Column::DeliveryDelay,
            Column::ReceiveMessageWaitTime,
            Column::HighThroughputFifo,
            Column::DeduplicationScope,
            Column::FifoThroughputLimit,
        ]
    }

    pub fn ids() -> Vec<ColumnId> {
        Self::all().iter().map(|c| c.id()).collect()
    }
}

impl TableColumn<Queue> for Column {
    fn name(&self) -> &str {
        Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
    }

    fn width(&self) -> u16 {
        let translated = translate_column(self.id(), self.default_name());
        translated.len().max(match self {
            Column::Name => 40,
            Column::Type => 10,
            Column::Created => UTC_TIMESTAMP_WIDTH as usize,
            Column::MessagesAvailable => 20,
            Column::MessagesInFlight => 20,
            Column::Encryption => 12,
            Column::ContentBasedDeduplication => 30,
            Column::LastUpdated => UTC_TIMESTAMP_WIDTH as usize,
            Column::VisibilityTimeout => 20,
            Column::MessageRetentionPeriod => 25,
            Column::MaximumMessageSize => 22,
            Column::DeliveryDelay => 15,
            Column::ReceiveMessageWaitTime => 25,
            Column::HighThroughputFifo => 22,
            Column::DeduplicationScope => 22,
            Column::FifoThroughputLimit => 22,
        }) as u16
    }

    fn render(&self, item: &Queue) -> (String, Style) {
        let text = match self {
            Column::Name => item.name.clone(),
            Column::Type => item.queue_type.clone(),
            Column::Created => format_unix_timestamp(&item.created_timestamp),
            Column::MessagesAvailable => item.messages_available.clone(),
            Column::MessagesInFlight => item.messages_in_flight.clone(),
            Column::Encryption => item.encryption.clone(),
            Column::ContentBasedDeduplication => item.content_based_deduplication.clone(),
            Column::LastUpdated => format_unix_timestamp(&item.last_modified_timestamp),
            Column::VisibilityTimeout => {
                if let Ok(seconds) = item.visibility_timeout.parse::<i32>() {
                    format_duration_seconds(seconds)
                } else {
                    item.visibility_timeout.clone()
                }
            }
            Column::MessageRetentionPeriod => {
                if let Ok(seconds) = item.message_retention_period.parse::<i32>() {
                    format_duration_seconds(seconds)
                } else {
                    item.message_retention_period.clone()
                }
            }
            Column::MaximumMessageSize => {
                if let Some(bytes_str) = item.maximum_message_size.split_whitespace().next() {
                    if let Ok(bytes) = bytes_str.parse::<i64>() {
                        format_bytes(bytes)
                    } else {
                        item.maximum_message_size.clone()
                    }
                } else {
                    item.maximum_message_size.clone()
                }
            }
            Column::DeliveryDelay => {
                if let Ok(seconds) = item.delivery_delay.parse::<i32>() {
                    format_duration_seconds(seconds)
                } else {
                    item.delivery_delay.clone()
                }
            }
            Column::ReceiveMessageWaitTime => {
                if let Ok(seconds) = item.receive_message_wait_time.parse::<i32>() {
                    format_duration_seconds(seconds)
                } else {
                    item.receive_message_wait_time.clone()
                }
            }
            Column::HighThroughputFifo => item.high_throughput_fifo.clone(),
            Column::DeduplicationScope => item.deduplication_scope.clone(),
            Column::FifoThroughputLimit => item.fifo_throughput_limit.clone(),
        };
        (text, Style::default())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_column_all() {
        assert_eq!(Column::all().len(), 16);
    }

    #[test]
    fn test_maximum_message_size_formatting() {
        let queue = Queue {
            name: "test".to_string(),
            url: String::new(),
            queue_type: "Standard".to_string(),
            created_timestamp: String::new(),
            messages_available: "0".to_string(),
            messages_in_flight: "0".to_string(),
            encryption: "Disabled".to_string(),
            content_based_deduplication: "Disabled".to_string(),
            last_modified_timestamp: String::new(),
            visibility_timeout: String::new(),
            message_retention_period: String::new(),
            maximum_message_size: "262144 bytes".to_string(),
            delivery_delay: String::new(),
            receive_message_wait_time: String::new(),
            high_throughput_fifo: "-".to_string(),
            deduplication_scope: "-".to_string(),
            fifo_throughput_limit: "-".to_string(),
            dead_letter_queue: "-".to_string(),
            messages_delayed: "0".to_string(),
            redrive_allow_policy: "-".to_string(),
            redrive_policy: "".to_string(),
            redrive_task_id: "-".to_string(),
            redrive_task_start_time: "-".to_string(),
            redrive_task_status: "-".to_string(),
            redrive_task_percent: "-".to_string(),
            redrive_task_destination: "-".to_string(),
        };

        let (text, _) = Column::MaximumMessageSize.render(&queue);
        assert_eq!(text, "262.14 KB");
    }

    #[test]
    fn test_duration_formatting() {
        let queue = Queue {
            name: "test".to_string(),
            url: String::new(),
            queue_type: "Standard".to_string(),
            created_timestamp: String::new(),
            messages_available: "0".to_string(),
            messages_in_flight: "0".to_string(),
            encryption: "Disabled".to_string(),
            content_based_deduplication: "Disabled".to_string(),
            last_modified_timestamp: String::new(),
            visibility_timeout: "30".to_string(),
            message_retention_period: "345600".to_string(),
            maximum_message_size: String::new(),
            delivery_delay: "0".to_string(),
            receive_message_wait_time: "20".to_string(),
            high_throughput_fifo: "-".to_string(),
            deduplication_scope: "-".to_string(),
            fifo_throughput_limit: "-".to_string(),
            dead_letter_queue: "-".to_string(),
            messages_delayed: "0".to_string(),
            redrive_allow_policy: "-".to_string(),
            redrive_policy: "".to_string(),
            redrive_task_id: "-".to_string(),
            redrive_task_start_time: "-".to_string(),
            redrive_task_status: "-".to_string(),
            redrive_task_percent: "-".to_string(),
            redrive_task_destination: "-".to_string(),
        };

        let (text, _) = Column::VisibilityTimeout.render(&queue);
        assert_eq!(text, "30s");

        let (text, _) = Column::MessageRetentionPeriod.render(&queue);
        assert_eq!(text, "4d");

        let (text, _) = Column::DeliveryDelay.render(&queue);
        assert_eq!(text, "0s");

        let (text, _) = Column::ReceiveMessageWaitTime.render(&queue);
        assert_eq!(text, "20s");
    }

    #[test]
    fn test_timestamp_formatting() {
        let queue = Queue {
            name: "test".to_string(),
            url: String::new(),
            queue_type: "Standard".to_string(),
            created_timestamp: "1609459200".to_string(),
            messages_available: "0".to_string(),
            messages_in_flight: "0".to_string(),
            encryption: "Disabled".to_string(),
            content_based_deduplication: "Disabled".to_string(),
            last_modified_timestamp: "1609459200".to_string(),
            visibility_timeout: String::new(),
            message_retention_period: String::new(),
            maximum_message_size: String::new(),
            delivery_delay: String::new(),
            receive_message_wait_time: String::new(),
            high_throughput_fifo: "-".to_string(),
            deduplication_scope: "-".to_string(),
            fifo_throughput_limit: "-".to_string(),
            dead_letter_queue: "-".to_string(),
            messages_delayed: "0".to_string(),
            redrive_allow_policy: "-".to_string(),
            redrive_policy: "".to_string(),
            redrive_task_id: "-".to_string(),
            redrive_task_start_time: "-".to_string(),
            redrive_task_status: "-".to_string(),
            redrive_task_percent: "-".to_string(),
            redrive_task_destination: "-".to_string(),
        };

        let (text, _) = Column::Created.render(&queue);
        assert!(text.contains("2021-01-01"));
        assert!(text.contains("(UTC)"));
    }
}

#[test]
fn test_column_ids_have_correct_prefix() {
    for col in Column::all() {
        assert!(
            col.id().starts_with("column.sqs.queue."),
            "Column ID '{}' should start with 'column.sqs.queue.'",
            col.id()
        );
    }
}