use thiserror::Error;
#[derive(Debug, Clone)]
pub struct ErrorRecommendation {
pub issue: String,
pub suggestion: String,
pub auto_fix: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum BoxenError {
#[error("Invalid border style: {message}")]
InvalidBorderStyle {
message: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Invalid color specification: {message}")]
InvalidColor {
message: String,
color_value: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Invalid dimensions: {message}")]
InvalidDimensions {
message: String,
width: Option<usize>,
height: Option<usize>,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Terminal size detection failed: {message}")]
TerminalSizeError {
message: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Text processing error: {message}")]
TextProcessingError {
message: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Configuration conflict: {message}")]
ConfigurationError {
message: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Input validation error: {message}")]
InputValidationError {
message: String,
field: String,
value: String,
recommendations: Vec<ErrorRecommendation>,
},
#[error("Rendering error: {message}")]
RenderingError {
message: String,
recommendations: Vec<ErrorRecommendation>,
},
}
impl BoxenError {
#[must_use]
pub fn invalid_dimensions(
message: String,
width: Option<usize>,
height: Option<usize>,
recommendations: Vec<ErrorRecommendation>,
) -> Self {
Self::InvalidDimensions {
message,
width,
height,
recommendations,
}
}
#[must_use]
pub fn configuration_error(message: String, recommendations: Vec<ErrorRecommendation>) -> Self {
Self::ConfigurationError {
message,
recommendations,
}
}
#[must_use]
pub fn invalid_color(
message: String,
color_value: String,
recommendations: Vec<ErrorRecommendation>,
) -> Self {
Self::InvalidColor {
message,
color_value,
recommendations,
}
}
#[must_use]
pub fn invalid_border_style(
message: String,
recommendations: Vec<ErrorRecommendation>,
) -> Self {
Self::InvalidBorderStyle {
message,
recommendations,
}
}
#[must_use]
pub fn terminal_size_error(message: String, recommendations: Vec<ErrorRecommendation>) -> Self {
Self::TerminalSizeError {
message,
recommendations,
}
}
#[must_use]
pub fn text_processing_error(
message: String,
recommendations: Vec<ErrorRecommendation>,
) -> Self {
Self::TextProcessingError {
message,
recommendations,
}
}
#[must_use]
pub fn input_validation_error(
message: String,
field: String,
value: String,
recommendations: Vec<ErrorRecommendation>,
) -> Self {
Self::InputValidationError {
message,
field,
value,
recommendations,
}
}
#[must_use]
pub fn rendering_error(message: String, recommendations: Vec<ErrorRecommendation>) -> Self {
Self::RenderingError {
message,
recommendations,
}
}
#[must_use]
pub fn recommendations(&self) -> Vec<ErrorRecommendation> {
match self {
Self::InvalidBorderStyle {
recommendations, ..
}
| Self::InvalidColor {
recommendations, ..
}
| Self::InvalidDimensions {
recommendations, ..
}
| Self::TerminalSizeError {
recommendations, ..
}
| Self::TextProcessingError {
recommendations, ..
}
| Self::ConfigurationError {
recommendations, ..
}
| Self::InputValidationError {
recommendations, ..
}
| Self::RenderingError {
recommendations, ..
} => recommendations.clone(),
}
}
#[must_use]
pub fn detailed_message(&self) -> String {
let base_message = self.to_string();
let recommendations = self.recommendations();
if recommendations.is_empty() {
return base_message;
}
let mut message = format!("{base_message}\n\nSuggestions:");
for (i, rec) in recommendations.iter().enumerate() {
use std::fmt::Write;
let _ = write!(message, "\n{}. {}: {}", i + 1, rec.issue, rec.suggestion);
if let Some(auto_fix) = &rec.auto_fix {
let _ = write!(message, "\n Auto-fix: {auto_fix}");
}
}
message
}
}
impl ErrorRecommendation {
#[must_use]
pub const fn new(issue: String, suggestion: String, auto_fix: Option<String>) -> Self {
Self {
issue,
suggestion,
auto_fix,
}
}
#[must_use]
pub const fn with_auto_fix(issue: String, suggestion: String, auto_fix: String) -> Self {
Self {
issue,
suggestion,
auto_fix: Some(auto_fix),
}
}
#[must_use]
pub const fn suggestion_only(issue: String, suggestion: String) -> Self {
Self {
issue,
suggestion,
auto_fix: None,
}
}
}
pub type BoxenResult<T> = Result<T, BoxenError>;
pub mod validation {
use super::{BoxenError, BoxenResult, ErrorRecommendation};
pub fn validate_text_input(text: &str) -> BoxenResult<()> {
if text.len() > 1_000_000 {
return Err(BoxenError::input_validation_error(
"Text input is too large and may cause performance issues".to_string(),
"text".to_string(),
format!("{} characters", text.len()),
vec![
ErrorRecommendation::suggestion_only(
"Text too large".to_string(),
"Consider splitting large text into smaller chunks or using height constraints".to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use height constraint".to_string(),
"Limit the visible height to prevent rendering issues".to_string(),
".height(50)".to_string(),
),
],
));
}
let line_count = text.lines().count();
if line_count > 1000 {
return Err(BoxenError::input_validation_error(
"Text has too many lines and may cause performance issues".to_string(),
"text".to_string(),
format!("{line_count} lines"),
vec![
ErrorRecommendation::suggestion_only(
"Too many lines".to_string(),
"Consider using height constraints to limit visible content".to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use height constraint".to_string(),
"Limit the visible height to improve performance".to_string(),
".height(30)".to_string(),
),
],
));
}
Ok(())
}
pub fn validate_spacing(
spacing: &crate::options::Spacing,
field_name: &str,
) -> BoxenResult<()> {
let max_reasonable_spacing = 100;
if spacing.top > max_reasonable_spacing {
return Err(BoxenError::input_validation_error(
format!("Top {field_name} value is unreasonably large"),
format!("{field_name}.top"),
spacing.top.to_string(),
vec![
ErrorRecommendation::suggestion_only(
"Excessive spacing".to_string(),
format!(
"Top {} of {} is very large and may cause layout issues",
field_name, spacing.top
),
),
ErrorRecommendation::with_auto_fix(
"Use reasonable spacing".to_string(),
"Consider using smaller spacing values".to_string(),
format!(".{field_name}(5)"),
),
],
));
}
if spacing.right > max_reasonable_spacing {
return Err(BoxenError::input_validation_error(
format!("Right {field_name} value is unreasonably large"),
format!("{field_name}.right"),
spacing.right.to_string(),
vec![ErrorRecommendation::suggestion_only(
"Excessive spacing".to_string(),
format!(
"Right {} of {} is very large and may cause layout issues",
field_name, spacing.right
),
)],
));
}
if spacing.bottom > max_reasonable_spacing {
return Err(BoxenError::input_validation_error(
format!("Bottom {field_name} value is unreasonably large"),
format!("{field_name}.bottom"),
spacing.bottom.to_string(),
vec![ErrorRecommendation::suggestion_only(
"Excessive spacing".to_string(),
format!(
"Bottom {} of {} is very large and may cause layout issues",
field_name, spacing.bottom
),
)],
));
}
if spacing.left > max_reasonable_spacing {
return Err(BoxenError::input_validation_error(
format!("Left {field_name} value is unreasonably large"),
format!("{field_name}.left"),
spacing.left.to_string(),
vec![ErrorRecommendation::suggestion_only(
"Excessive spacing".to_string(),
format!(
"Left {} of {} is very large and may cause layout issues",
field_name, spacing.left
),
)],
));
}
Ok(())
}
pub fn validate_dimensions(width: Option<usize>, height: Option<usize>) -> BoxenResult<()> {
if let Some(w) = width {
if w == 0 {
return Err(BoxenError::input_validation_error(
"Width cannot be zero".to_string(),
"width".to_string(),
"0".to_string(),
vec![ErrorRecommendation::with_auto_fix(
"Zero width".to_string(),
"Width must be at least 1 character".to_string(),
".width(10)".to_string(),
)],
));
}
if w > 10000 {
return Err(BoxenError::input_validation_error(
"Width is unreasonably large".to_string(),
"width".to_string(),
w.to_string(),
vec![
ErrorRecommendation::suggestion_only(
"Excessive width".to_string(),
format!("Width of {w} is very large and may cause display issues"),
),
ErrorRecommendation::with_auto_fix(
"Use reasonable width".to_string(),
"Consider using a more reasonable width value".to_string(),
".width(80)".to_string(),
),
],
));
}
}
if let Some(h) = height {
if h == 0 {
return Err(BoxenError::input_validation_error(
"Height cannot be zero".to_string(),
"height".to_string(),
"0".to_string(),
vec![ErrorRecommendation::with_auto_fix(
"Zero height".to_string(),
"Height must be at least 1 line".to_string(),
".height(5)".to_string(),
)],
));
}
if h > 1000 {
return Err(BoxenError::input_validation_error(
"Height is unreasonably large".to_string(),
"height".to_string(),
h.to_string(),
vec![
ErrorRecommendation::suggestion_only(
"Excessive height".to_string(),
format!("Height of {h} is very large and may cause display issues"),
),
ErrorRecommendation::with_auto_fix(
"Use reasonable height".to_string(),
"Consider using a more reasonable height value".to_string(),
".height(30)".to_string(),
),
],
));
}
}
Ok(())
}
pub fn validate_title(title: &str) -> BoxenResult<()> {
if title.len() > 200 {
return Err(BoxenError::input_validation_error(
"Title is too long".to_string(),
"title".to_string(),
format!("{} characters", title.len()),
vec![
ErrorRecommendation::suggestion_only(
"Long title".to_string(),
"Very long titles may be truncated or cause layout issues".to_string(),
),
ErrorRecommendation::with_auto_fix(
"Shorten title".to_string(),
"Consider using a shorter, more concise title".to_string(),
format!(".title(\"{}\")", &title[..20.min(title.len())]),
),
],
));
}
if title.chars().any(|c| c.is_control() && c != '\t') {
return Err(BoxenError::input_validation_error(
"Title contains invalid control characters".to_string(),
"title".to_string(),
title.to_string(),
vec![ErrorRecommendation::suggestion_only(
"Control characters".to_string(),
"Titles should not contain control characters (except tabs)".to_string(),
)],
));
}
Ok(())
}
pub fn validate_all_options(
text: &str,
options: &crate::options::BoxenOptions,
) -> BoxenResult<()> {
validate_text_input(text)?;
validate_spacing(&options.padding, "padding")?;
validate_spacing(&options.margin, "margin")?;
let terminal_width = crate::terminal::get_terminal_width();
let terminal_height = crate::terminal::get_terminal_height();
let actual_width = options.width.as_ref().map(|w| w.calculate(terminal_width));
let actual_height = options
.height
.as_ref()
.map(|h| h.calculate(terminal_height.unwrap_or(24)));
validate_dimensions(actual_width, actual_height)?;
if let Some(ref title) = options.title {
validate_title(title)?;
}
if let Some(ref color) = options.border_color {
crate::color::validate_color(color).map_err(|_e| {
BoxenError::input_validation_error(
"Invalid border color".to_string(),
"border_color".to_string(),
format!("{color:?}"),
vec![
ErrorRecommendation::suggestion_only(
"Invalid color".to_string(),
"Use a valid color name (red, blue, etc.) or hex code (#FF0000)"
.to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use valid color".to_string(),
"Try using a standard color name".to_string(),
".border_color(\"blue\")".to_string(),
),
],
)
})?;
}
if let Some(ref color) = options.background_color {
crate::color::validate_color(color).map_err(|_e| {
BoxenError::input_validation_error(
"Invalid background color".to_string(),
"background_color".to_string(),
format!("{color:?}"),
vec![
ErrorRecommendation::suggestion_only(
"Invalid color".to_string(),
"Use a valid color name (red, blue, etc.) or hex code (#FF0000)"
.to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use valid color".to_string(),
"Try using a standard color name".to_string(),
".background_color(\"white\")".to_string(),
),
],
)
})?;
}
Ok(())
}
}